Next.js: Otimização de Imagens & Fontes

Última atualização: 2026-08-26

Imagens e fontes representam mais de 70% do tamanho de uma página — otimizá-las pode reduzir o LCP de 4 segundos para 1 segundo.

1. O Que Você Vai Aprender



2. Uma História Real de uma Engenheira de Performance Front-End

(1) Ponto de Dor: Uma Única Imagem Pode Arruinar a Performance de uma Página Inteira

Diana é engenheira DevOps da equipe TaskFlow. Ela descobriu que o LCP (Largest Contentful Paint) da página Dashboard estava em 4,8 segundos:

"O banner no topo da página é uma imagem PNG de 5MB (tamanho original 4000×3000px), carregada diretamente com <img src="/banner.png" />. O Chrome DevTools mostra: decodificação levou 800 ms, Cumulative Layout Shift (CLS) foi de 0,45 e o carregamento da fonte bloqueou a renderização por 600 ms."

Problema Impacto Métrica CWV
Imagem original 5MB sem compressão Download 3,2s ❌ LCP 4,8s
Sem atributos de dimensão Layout da página muda constantemente ❌ CLS 0,45
Bloqueio de renderização de fonte Tela branca prolongada ❌ FCP 2,1s
Imagens não responsivas Imagens 4K também carregam em dispositivos móveis ❌ Desperdício de dados

(2) A Solução next/image + next/font

Use o componente <Image> para otimizar imagens automaticamente e next/font para eliminar o bloqueio de fontes.

TSX
import Image from 'next/image'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'], display: 'swap' })

export default function Hero() {
  return (
    <div className={inter.className}>
      <Image
        src="/banner.webp"
        alt="Banner do TaskFlow"
        width={1200}
        height={400}
        priority
        placeholder="blur"
        blurDataURL="data:image/webp;base64,..."
      />
    </div>
  )
}

(3) Resultado

Métrica Antes da Otimização Depois da Otimização Melhoria
LCP 4,8s 1,2s 75% ↓
CLS 0,45 0,02 96% ↓
Tamanho da Imagem 5 MB 120 KB 97% ↓
FCP 2,1s 0,8s 62% ↓


3. Propriedades Principais do Componente next/image

(1) Tabela de Referência de Propriedades

Propriedade Tipo Obrigatória Descrição
src string / StaticImport Caminho da imagem ou import estático
width number ✅ (estático) Largura da imagem (px)
height number ✅ (estático) Altura da imagem (px)
alt string Texto alternativo (acessibilidade)
priority boolean Preloading de imagem LCP
placeholder 'blur' / 'empty' Placeholder de carregamento
blurDataURL string Requer blur Placeholder blur em base64
sizes string Breakpoint responsivo
fill boolean Preencher o container pai
quality number Qualidade de compressão (1–100)
loading 'lazy' / 'eager' Estratégia de carregamento lazy
100%
graph TB
    A[<Image src="/photo.jpg"/>] --> B{Build do Next.js}
    B --> C[Recodificação Server-Side com Sharp]
    C --> D[Versão WebP<br/>1200w / 800w / 400w]
    C --> E[Versão AVIF<br/>1200w / 800w / 400w]
    D --> F[Seleção do Navegador<br/><picture> Ajuste Automático]
    E --> F

    style B fill:#cce5ff
    style C fill:#d4edda
    style F fill:#fff3cd

(2) Exemplo de configuração responsiva

TS
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'images.unsplash.com' },
      { protocol: 'https', hostname: 'cdn.taskflow.io', port: '', pathname: '/assets/**' }
    ],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
  }
}

export default nextConfig
⚠️ Nota: images.domains foi depreciado no Next.js 15 e versões posteriores; use remotePatterns (que suporta correspondência de caminho com wildcard).

▶ Exemplo: Imagem Remota + Responsiva + Placeholder Blur

Output:

TEXT 📖 Somente leitura
Módulo TypeScript executado com sucesso.
TSX
// components/TeamPhoto.tsx
import Image from 'next/image'

export default function TeamPhoto() {
  return (
    <Image
      src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200"
      alt="Foto da Equipe TaskFlow"
      width={1200}
      height={600}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 1200px"
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
      priority
      className="rounded-lg"
    />
  )
}

Output:

TEXT 📖 Somente leitura
Renderiza a UI do componente TeamPhoto.
💡 Dica: sizes informa ao navegador a largura de exibição de uma imagem em diferentes viewports, ajudando o navegador a selecionar o tamanho de imagem mais adequado. Se isso não for configurado, dispositivos móveis podem acabar carregando a imagem de 1920px também.



4. placeholder="blur" e blurDataURL

(1) Dois Esquemas de Placeholder

Solução Método de Geração Volume Cenários Aplicáveis
Import estático com "blur" Gerado automaticamente pelo Next.js ~2KB Imagem local (import img from './photo.jpg')
blurDataURL manual base64 gerado por ferramenta ~200B Imagens remotas, imagens animadas
Biblioteca de Placeholder Gerado em runtime ~500B Requer URL remota dinâmica

(2) Import estático com blur automático

TSX
// ✅ Import Estático — Next.js Gera blurDataURL Automaticamente
import teamPhoto from '@/public/team.jpg'

export default function AboutPage() {
  return (
    <Image
      src={teamPhoto}
      alt="Foto da Equipe"
      placeholder="blur"          // Usa automaticamente o blur gerado
      className="rounded-xl"
    />
  )
}

(3) Ferramenta para Gerar Pré-visualizações Blur de Imagens Remotas

TEXT 📖 Somente leitura
# Uso: biblioteca plaiceholder
npm install plaiceholder
TS
// lib/getBlurData.ts — Geração de blurURL para Imagem Remota
import { getPlaiceholder } from 'plaiceholder'

export async function getBlurDataURL(src: string) {
  try {
    const response = await fetch(src)
    const buffer = Buffer.from(await response.arrayBuffer())
    const { base64 } = await getPlaiceholder(buffer)
    return base64
  } catch {
    return undefined
  }
}

▶ Exemplo: Imagem remota dinâmica + placeholder blur

Output:

TEXT 📖 Somente leitura
Código TypeScript executado com sucesso.
TSX
// app/team/page.tsx — Lista de Avatares da Equipe + Placeholder blur
import Image from 'next/image'
import { getBlurDataURL } from '@/lib/getBlurData'

const members = [
  { name: 'Alice', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200' },
  { name: 'Bob', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200' },
]

export default async function TeamPage() {
  const membersWithBlur = await Promise.all(
    members.map(async (m) => ({ ...m, blur: await getBlurDataURL(m.avatar) }))
  )

  return (
    <div style={{ display: 'flex', gap: '1rem' }}>
      {membersWithBlur.map((m) => (
        <Image
          key={m.name}
          src={m.avatar}
          alt={m.name}
          width={100}
          height={100}
          placeholder="blur"
          blurDataURL={m.blur}
          className="rounded-full"
        />
      ))}
    </div>
  )
}

Output:

TEXT 📖 Somente leitura
Renderiza uma lista mapeando membersWithBlur, exibindo cada m.


5. next/font — Otimização de Fontes

(1) O Impacto do Carregamento de Fontes na Performance

100%
graph TB
    A[@font-face Tradicional] --> B[Bloqueio de Renderização<br/>FOIT]
    A --> C[Deslocamento de Layout<br/>FOUT]
    B --> D[FCP Atrasado em 300-600ms]
    C --> E[CLS 0,1-0,3]

    F[next/font] --> G[display:swap<br/>Usa fonte de fallback imediatamente]
    F --> H[size-adjust<br/>Elimina Deslocamentos de Layout]
    F --> I[preload Preload<br/>Caminho Crítico Sem Bloqueio]
    F --> J[CSS size-adjust<br/>Sobreposição de Métricas Tipográficas]

    style A fill:#f8d7da
    style F fill:#d4edda
Problema Carregamento de Fonte Tradicional Solução next/font
FOIT (fonte não visível) Texto não é exibido até a fonte ser baixada display:swap Exibe fonte de fallback imediatamente
CLS (Cumulative Layout Shift) Tamanho muda antes e depois do carregamento da fonte size-adjust Sobrescreve métricas da fonte
Requisições Adicionais Download serializado de múltiplos arquivos de fonte CSS Inline + Preloading
Atraso do Google Fonts Requisições lentas de CDN cross-border Baixado durante o build, zero requisições em runtime

(2) Google Variable Fonts

TSX
// app/layout.tsx — Google Variable Fonts
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
  variable: '--font-inter',         // Padrão de Variável CSS
  weight: 'variable'                // Intervalo de Fonte Variável
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono'
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt" className={`${inter.variable} ${robotoMono.variable}`}>
      <body style={{ fontFamily: 'var(--font-inter)' }}>
        <code style={{ fontFamily: 'var(--font-roboto-mono)' }}>{children}</code>
      </body>
    </html>
  )
}

(3) Ajuste de Tamanho de Fonte (size-adjust)

TSX
// Fontes Locais Personalizadas + size-adjust para prevenção de CLS
import localFont from 'next/font/local'

const myFont = localFont({
  src: './fonts/CustomFont.woff2',
  display: 'swap',
  adjustment: {
    ascent: 90,
    descent: 20,
    lineGap: 10,
    sizeAdjust: '105%'
  }
})

▶ Exemplo: Combinando Múltiplas Fontes + Tailwind CSS

Output:

TEXT 📖 Somente leitura
Renderiza a UI do componente Component.
TSX
// app/layout.tsx — Fonte de Texto + Fonte de Código + Tailwind
import { Inter, JetBrains_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-sans',
  display: 'swap'
})

const jetbrainsMono = JetBrains_Mono({
  subsets: ['latin'],
  variable: '--font-mono',
  display: 'swap'
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt" className={`${inter.variable} ${jetbrainsMono.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  )
}

Output:

TEXT 📖 Somente leitura
Renderiza a UI do componente RootLayout.
CSS
/* tailwind.config.ts */
import type { Config } from 'tailwindcss'

export default {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-sans)'],
        mono: ['var(--font-mono)']
      }
    }
  }
} satisfies Config


6. Princípios de Otimização de Imagens: Sharp + WebP/AVIF

(1) Pipeline de Otimização do Sharp

100%
graph LR
    A[Imagem de origem<br/>PNG/JPEG 5MB] --> B[Processamento Server-Side com Sharp]
    B --> C[Redimensionar<br/>1200w / 800w / 400w]
    B --> D[Compressão de Qualidade<br/>75% de qualidade]
    B --> E[Conversão de Formato]
    E --> F[WebP<br/>~80% ↓ Volume]
    E --> G[AVIF<br/>~90% ↓ Volume]
    F --> H[<picture> Auto-negociação<br/>Navegador: Seleciona o Melhor Formato]

    style B fill:#d4edda
    style H fill:#cce5ff
Formato Taxa de Compressão (vs. JPEG) Suporte dos Navegadores Velocidade de Decodificação
JPEG Referência 100% Mais rápida
WebP ~30% de redução no tamanho 96% Bastante rápida
AVIF ~50% de redução no tamanho 93% Mais lenta
HEIC ~50% de redução no tamanho Somente Safari Mais rápida

(2) Configurar Qualidade Global de Imagem

TS
// next.config.ts — Configurações Globais de Otimização de Imagem
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 30,  // 30 dias de cache
    dangerouslyAllowSVG: false,
    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
  }
}

▶ Exemplo: Teste de Qualidade e Tamanho de Imagem

Output:

TEXT 📖 Somente leitura
Módulo TypeScript executado com sucesso.
TSX
// components/ImageTest.tsx — Comparação de Diferentes Níveis de Qualidade para a Mesma Imagem
import Image from 'next/image'

export default function ImageTest() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
      <div>
        <h3>Quality=50</h3>
        <Image src="/test.jpg" alt="q50" width={400} height={300} quality={50} />
      </div>
      <div>
        <h3>Quality=75(Padrão)</h3>
        <Image src="/test.jpg" alt="q75" width={400} height={300} quality={75} />
      </div>
      <div>
        <h3>Quality=100</h3>
        <Image src="/test.jpg" alt="q100" width={400} height={300} quality={100} />
      </div>
    </div>
  )
}

Output:

TEXT 📖 Somente leitura
Renderiza: Quality=50
Texto visível: Quality=50 | Quality=75(Padrão) | Quality=100
💡 Dica: A diferença entre 75% e 100% é quase imperceptível a olho nu, mas o volume difere por um fator de 3 a 4 vezes. Para ambientes de produção, recomenda-se 75–85%.



7. Otimização Prática de Core Web Vitals

(1) As Três Métricas Principais do CWV

Métrica Pontuação Máxima Gap Medidas de Otimização
LCP (Largest Contentful Paint) ≤2,5s >4,0s Preload de imagens LCP + compressão + CDN
CLS (Cumulative Layout Shift) ≤0,1 >0,25 Dimensões fixas de imagem + ajuste de tamanho de fonte
INP (Interaction to Next Paint) ≤200 ms >500 ms Reduzir execução JS + code splitting

(2) Checklist de Otimização de Imagem LCP

TSX
// ✅ Configurações Corretas de Imagem LCP
export default function HeroBanner() {
  return (
    <Image
      src="/hero-banner.webp"
      alt="Hero do TaskFlow"
      width={1440}
      height={600}
      priority          // Diz ao Next.js para fazer preload desta imagem
      quality={85}
      placeholder="blur"
      sizes="100vw"
      style={{ width: '100%', height: 'auto' }}  // Responsivo
    />
  )
}

(3) Eliminando CLS de Fontes

TSX
// layout.tsx — Solução Completa para Eliminação de CLS de Fontes
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  adjustFontFallback: true,   // Next.js Calcula size-adjust Automaticamente
  fallback: ['system-ui', 'sans-serif']
})
🔥 Erro Comum: Não carregue mais de 3 pesos de fonte na mesma página. Cada variante adiciona aproximadamente 50 KB ao arquivo WOFF2.

▶ Exemplo: Componente de Monitoramento CWV

Output:

TEXT 📖 Somente leitura
Renderiza a UI do componente Component.
TSX
// components/WebVitals.tsx — Envia CWV para a plataforma de analytics
'use client'
import { useReportWebVitals } from 'next/web-vitals'

export default function WebVitalsReporter() {
  useReportWebVitals((metric) => {
    console.log(metric)  // Visualizar durante o desenvolvimento

    // Envio do Ambiente de Produção para API de Analytics
    if (process.env.NODE_ENV === 'production') {
      const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        rating: metric.rating,
        id: metric.id
      })
      navigator.sendBeacon('/api/analytics', body)
    }
  })

  return null
}

Output:

TEXT 📖 Somente leitura
Renderiza a UI do componente WebVitalsReporter.
TSX
// app/layout.tsx — Montagem do Monitoramento
import WebVitalsReporter from '@/components/WebVitals'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <WebVitalsReporter />
      </body>
    </html>
  )
}


8. Exemplo Completo: Otimização Abrangente de Imagens, Fontes e CWV da Homepage

TSX
// app/layout.tsx — Layout Raiz,Fontes Globais + Imagem + Monitoramento Vitals
import { Inter, Noto_Sans_SC } from 'next/font/google'
import WebVitalsReporter from '@/components/WebVitals'
import type { Metadata } from 'next'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
  adjustFontFallback: true
})

const notoSansSC = Noto_Sans_SC({
  subsets: ['latin'],
  weight: 'variable',
  variable: '--font-noto',
  display: 'swap',
  adjustFontFallback: true
})

export const metadata: Metadata = {
  title: 'TaskFlow - Plataforma de Colaboração em Equipe',
  description: 'TaskFlow Ajuda mais de 10.000 Equipes a Colaborar com Alta Eficiência'
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt" className={`${inter.variable} ${notoSansSC.variable}`}>
      <body style={{ fontFamily: 'var(--font-inter), var(--font-noto), sans-serif' }}>
        {children}
        <WebVitalsReporter />
      </body>
    </html>
  )
}
TSX
// app/page.tsx — Hero da Home + Otimização LCP
import Image from 'next/image'
import Link from 'next/link'

export default function HomePage() {
  return (
    <div>
      {/* Região Hero — Imagem LCP */}
      <div style={{ position: 'relative', width: '100%', height: 500 }}>
        <Image
          src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1920&q=85"
          alt="Trabalho em Equipe TaskFlow"
          fill
          priority
          sizes="100vw"
          style={{ objectFit: 'cover' }}
          placeholder="blur"
          blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
        />
        <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <div style={{ textAlign: 'center', color: '#fff' }}>
            <h1 style={{ fontSize: 'clamp(2rem, 5vw, 4rem)' }}>TaskFlow</h1>
            <p style={{ fontSize: '1.25rem' }}>Plataforma de colaboração usada por mais de 10.000 equipes</p>
            <Link href="/signup" style={{ display: 'inline-block', padding: '0.75rem 2rem', background: '#4f46e5', color: '#fff', borderRadius: 8 }}>
              Comece Gratuitamente
            </Link>
          </div>
        </div>
      </div>

      {/* Região de Funcionalidades — Carregamento Lazy de Imagens */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '2rem', padding: '4rem 2rem' }}>
        {features.map((feature) => (
          <div key={feature.title}>
            <Image
              src={feature.icon}
              alt={feature.title}
              width={48}
              height={48}
              loading="lazy"
            />
            <h3>{feature.title}</h3>
            <p>{feature.description}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

const features = [
  { title: 'Gestão de Projetos', description: 'Quadro + Gráfico de Gantt', icon: '/icons/project.svg' },
  { title: 'Colaboração em Tempo Real', description: 'Edição Simultânea por Múltiplos Usuários', icon: '/icons/team.svg' },
  { title: 'Análise Inteligente', description: 'Insights Impulsionados por IA', icon: '/icons/analytics.svg' },
]

❓ Perguntas Frequentes

P: Qual é a diferença entre next/image e fill em width/height? R: fill faz a imagem preencher seu container pai (que deve ter position: relative) sem especificar um tamanho fixo; é usado em conjunto com objectFit. Largura e altura estáticas exigem a especificação explícita das dimensões da imagem para calcular o espaço ocupado e prevenir CLS.

P: Como gerar uma imagem blur para placeholder="blur"? R: O Next.js gera automaticamente durante o import estático. Para imagens remotas, você precisa fornecer manualmente blurDataURL (uma miniatura blur codificada em base64), que pode ser gerada usando a biblioteca plaiceholder ou uma ferramenta online (como https://png-pixel.com para gerar uma imagem blur 4x4).

P: display=swap e next/font no Google Fonts são o mesmo que display:swap? R: Têm o mesmo efeito, mas são implementados de forma diferente. O método tradicional envolve adicionar &display=swap à tag <link> e substituir somente após o download da fonte terminar. O next/font baixa o arquivo de fonte durante o build e o inline na página via CSS, resultando em zero requisições em runtime, além de gerar automaticamente size-adjust para eliminar CLS.

P: Por que minha imagem LCP não está sendo pré-carregada automaticamente? R: Verifique se o atributo priority está definido na imagem LCP. O Next.js apenas adiciona <link rel="preload"> a imagens que possuem priority. Além disso, uma configuração incorreta de remotePatterns também pode causar falha na otimização.

P: Qual devo escolher, AVIF ou WebP? R: Configure ambos e deixe o navegador escolher automaticamente usando a tag <picture>. AVIF oferece maior compressão, mas é mais lento para decodificar, sendo adequado para imagens não-LCP; WebP tem melhor compatibilidade (96%) e é adequado para imagens LCP. O Next.js gera ambos os formatos por padrão.


📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Configure next/image no projeto, use remotePatterns para habilitar a fonte de imagem Unsplash e implemente um componente de imagem responsivo (incluindo sizes e placeholder="blur").

  2. Exercício Avançado (⭐⭐): Compare o impacto de diferentes qualidades de imagem (50/75/100) e formatos (JPEG/WebP/AVIF) no LCP da página, registre o tamanho do arquivo e o tempo de carregamento para cada combinação e determine a configuração ideal.

  3. Desafio (⭐⭐⭐): Construa uma solução completa de otimização CWV: next/font configure duas fontes variáveis (fonte de texto + fonte de código), configure imagens LCP com priority + placeholders blur, useReportWebVitals reporte métricas para uma API personalizada e, por fim, alcance uma pontuação de Performance no Lighthouse ≥ 90.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%