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
- Os atributos principais do componente (
next/image/<Image>/remotePatterns/sizes/priority/placeholder) - Princípios de Otimização de Imagens: Recodificação server-side com Sharp, codificação adaptativa WebP / AVIF
next/fontOtimização de fontes (Google Web Fonts,display:swap,size-adjust, preloading)- Estratégias de Otimização de Performance para Core Web Vitals (LCP / CLS / INP)
- Segurança de Imagens Remotas e Migração da Depreciação do
images.domains
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 enext/fontpara eliminar o bloqueio de fontes.
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 |
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
// 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
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:
Módulo TypeScript executado com sucesso.
// 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:
Renderiza a UI do componente TeamPhoto.
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
// ✅ 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
# Uso: biblioteca plaiceholder
npm install plaiceholder
// 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:
Código TypeScript executado com sucesso.
// 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:
Renderiza uma lista mapeando membersWithBlur, exibindo cada m.
5. next/font — Otimização de Fontes
(1) O Impacto do Carregamento de Fontes na Performance
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
// 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)
// 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:
Renderiza a UI do componente Component.
// 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:
Renderiza a UI do componente RootLayout.
/* 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
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
// 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:
Módulo TypeScript executado com sucesso.
// 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:
Renderiza: Quality=50
Texto visível: Quality=50 | Quality=75(Padrão) | Quality=100
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
// ✅ 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
// 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']
})
▶ Exemplo: Componente de Monitoramento CWV
Output:
Renderiza a UI do componente Component.
// 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:
Renderiza a UI do componente WebVitalsReporter.
// 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
// 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>
)
}
// 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/imageefillemwidth/height? R:fillfaz a imagem preencher seu container pai (que deve terposition: relative) sem especificar um tamanho fixo; é usado em conjunto comobjectFit. 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 manualmenteblurDataURL(uma miniatura blur codificada em base64), que pode ser gerada usando a bibliotecaplaiceholderou uma ferramenta online (comohttps://png-pixel.compara gerar uma imagem blur 4x4).
P:
display=swapenext/fontno Google Fonts são o mesmo quedisplay: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. Onext/fontbaixa 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 automaticamentesize-adjustpara eliminar CLS.
P: Por que minha imagem LCP não está sendo pré-carregada automaticamente? R: Verifique se o atributo
priorityestá definido na imagem LCP. O Next.js apenas adiciona<link rel="preload">a imagens que possuempriority. Além disso, uma configuração incorreta deremotePatternstambé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
next/imageOferece otimização automática: recodificação com Sharp, codificação adaptativa WebP/AVIF, imagens responsivas multi-tamanho e lazy loadingremotePatternsSubstitui o depreciadoimages.domainse suporta correspondência segura com wildcards de caminhoplaceholder="blur"Combinado comblurDataURLelimina o flash branco quando as imagens carregamnext/fontBaixa Google Fonts durante o build para eliminar FOIT e FOUT, e ajusta automaticamente as métricas de fonte para prevenir CLS- Imagens LCP devem ter
prioritydefinido para preloading; imagens não acima da dobra devem usarloading="lazy"para lazy loading - Otimização de Core Web Vitals: Compressão de Imagem (LCP), Dimensões Fixas (CLS) e Code Splitting de JavaScript (INP)
📝 Exercícios
-
Exercício Básico (⭐): Configure
next/imageno projeto, useremotePatternspara habilitar a fonte de imagem Unsplash e implemente um componente de imagem responsivo (incluindosizeseplaceholder="blur"). -
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.
-
Desafio (⭐⭐⭐): Construa uma solução completa de otimização CWV:
next/fontconfigure duas fontes variáveis (fonte de texto + fonte de código), configure imagens LCP compriority+ placeholdersblur,useReportWebVitalsreporte métricas para uma API personalizada e, por fim, alcance uma pontuação de Performance no Lighthouse ≥ 90.