Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Instale a biblioteca web-vitals para medição de usuários reais
// npm install web-vitals
// app/components/WebVitals.tsx
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
const { name, value, rating } = metric;
// Envie para seu endpoint de análise
fetch("/api/vitals", {
method: "POST",
body: JSON.stringify({ name, value, rating }),
});
});
return null;
}
// Metas:
// LCP (Largest Contentful Paint): abaixo de 2,5s
// INP (Interaction to Next Paint): abaixo de 200ms
// CLS (Cumulative Layout Shift): abaixo de 0,1
// TTFB (Time to First Byte): abaixo de 800ms
// FCP (First Contentful Paint): abaixo de 1,8sQuando usar isso: Antes e depois de cada implantação em produção. Os Core Web Vitals são fatores de classificação do Google e se correlacionam diretamente com o engajamento do usuário e as taxas de conversão.
// ---- ANTES: Vitals ruins - LCP 4,1s, INP 380ms, CLS 0,28 ----
// app/page.tsx - Componente cliente com recursos bloqueadores
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion"; // Importação bloqueadora de 44KB
export default function HomePage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
// Busca após a hidratação - atrasa o LCP
useEffect(() => {
fetch("/api/products")
.then((r) => r.json())
.then((data) => {
setProducts(data);
setLoading(false);
});
}, []);
// Problema de INP: filtragem síncrona pesada a cada clique
const handleFilter = (category: string) => {
const sorted = products
.filter((p) => p.category === category)
.sort((a, b) => a.name.localeCompare(b.name))
.map((p) => ({ ...p, score: computeExpensiveScore(p) })); // 200ms+ síncrono
setProducts(sorted);
};
if (loading) return <div>Carregando...</div>;
return (
<div>
{/* CLS: Imagem sem dimensões */}
<img src="/hero.jpg" alt="Hero" />
{/* CLS: Fonte externa causa reflow de texto */}
<h1 style={{ fontFamily: "'Fancy Font', serif" }}>Nossos Produtos</h1>
{/* CLS: Conteúdo dinâmico empurra o layout */}
{products.length > 0 && (
<p className="text-sm text-gray-500">{products.length} produtos encontrados</p>
)}
<div className="flex gap-2 my-4">
{categories.map((cat) => (
<button key={cat} onClick={() => handleFilter(cat)}>
{cat}
</button>
))}
</div>
<motion.div className="grid grid-cols-3 gap-4">
{products.map((p) => (
<div key={p.id}>
<img src={p.image} alt={p.name} /> {/* Sem lazy loading, sem otimização */}
<p>{p.name} - ${p.price}</p>
</div>
))}
</motion.div>
</div>
);
}
// ---- DEPOIS: Bons vitals - LCP 1,8s, INP 85ms, CLS 0,03 ----
// app/page.tsx - Server Component com recursos otimizados
import Image from "next/image";
import { Inter } from "next/font/google";
import { Suspense } from "react";
import { db } from "@/lib/db";
import { ProductGrid } from "./ProductGrid";
import heroImage from "@/public/hero.jpg";
const inter = Inter({ subsets: ["latin"], display: "swap" });
export default async function HomePage() {
// Correção de LCP: Busca no lado do servidor - dados disponíveis na primeira renderização
const products = await db.product.findMany({
orderBy: { name: "asc" },
take: 50,
});
return (
<div className={inter.className}>
{/* Correção de LCP: Imagem otimizada com pré-carga prioritária */}
<Image
src={heroImage}
alt="Vitrine de produtos apresentando nossa última coleção"
priority
placeholder="blur"
sizes="100vw"
className="w-full h-auto"
/>
{/* Correção de CLS: Fonte auto-hospedada via next/font - zero mudança de layout */}
<h1 className="text-4xl font-bold mt-8">Nossos Produtos</h1>
{/* Correção de CLS: Espaço reservado para a contagem de produtos */}
<p className="text-sm text-gray-500 h-6">
{products.length} produtos encontrados
</p>
<Suspense fallback={<div className="grid grid-cols-3 gap-4 h-96" />}>
<ProductGrid initialProducts={products} />
</Suspense>
</div>
);
}
// ProductGrid.tsx - Componente cliente com interações otimizadas
"use client";
import { useState, useTransition } from "react";
import Image from "next/image";
export function ProductGrid({ initialProducts }: { initialProducts: Product[] }) {
const [products, setProducts] = useState(initialProducts);
const [isPending, startTransition] = useTransition();
// Correção de INP: useTransition marca a filtragem como não urgente
const handleFilter = (category: string) => {
startTransition(() => {
const filtered = initialProducts
.filter((p) => category === "all" || p.category === category)
.sort((a, b) => a.name.localeCompare(b.name));
setProducts(filtered);
});
};
return (
<div>
<div className="flex gap-2 my-4">
<button onClick={() => handleFilter("all")}>Todos</button>
{categories.map((cat) => (
<button key={cat} onClick={() => handleFilter(cat)}>
{cat}
</button>
))}
</div>
<div
className={`grid grid-cols-3 gap-4 ${isPending ? "opacity-60" : ""}`}
>
{products.map((p, index) => (
<div key={p.id}>
{/* Correção de CLS: largura + altura evitam mudança de layout */}
{/* LCP: prioridade nas 3 primeiras imagens */}
<Image
src={p.image}
alt={p.name}
width={400}
height={300}
priority={index < 3}
sizes="(max-width: 768px) 100vw, 33vw"
className="rounded-lg"
/>
<p className="mt-2 font-medium">{p.name} - ${p.price}</p>
</div>
))}
</div>
</div>
);
}O que isso demonstra:
useTransition adia a filtragem pesada = 380ms para 85ms (78% mais rápido)next/image + next/font + espaço reservado = 0,28 para 0,03 (redução de 89%)framer-motion removida: economia de 44KB do pacote inicialMedindo com a biblioteca web-vitals:
// app/vitals.ts
import { onCLS, onINP, onLCP, onFCP, onTTFB } from "web-vitals";
function sendToAnalytics(metric: { name: string; value: number; rating: string }) {
const body = JSON.stringify(metric);
// Use sendBeacon para confiabilidade durante o descarregamento da página
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/vitals", body);
} else {
fetch("/api/vitals", { method: "POST", body, keepalive: true });
}
}
export function reportWebVitals() {
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}Otimização de INP com useTransition e useDeferredValue:
"use client";
import { useState, useTransition, useDeferredValue } from "react";
function SearchableList({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// Filtragem adiada - a digitação permanece responsiva
const filtered = items.filter((item) =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Pesquisar..."
/>
<div className={isStale ? "opacity-50" : ""}>
{filtered.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
);
}Checklist de prevenção de CLS:
// 1. Sempre defina as dimensões da imagem
<Image src={url} alt="" width={800} height={600} />
// 2. Reserve espaço para conteúdo dinâmico
<div className="min-h-[200px]">
{data ? <Content data={data} /> : <Skeleton />}
</div>
// 3. Use next/font em vez de stylesheets externos
import { Inter } from "next/font/google";
// 4. Evite inserir conteúdo acima do conteúdo existente
// RUIM: O banner aparece na dobra após o carregamento
// BOM: Reserve espaço para o banner desde o início
// 5. Use contain CSS para anúncios e incorporações
<div style={{ contain: "layout", minHeight: 250 }}>
<AdSlot />
</div>web-vitals fornece tipos TypeScript para todos os objetos de métrica: CLSMetric, INPMetric, LCPMetric, etc.useReportWebVitals de next/web-vitals aceita um callback com o tipo NextWebVitalsMetric.rating é tipado como "good" | "needs-improvement" | "poor".Medir em desenvolvimento em vez de produção - O modo de desenvolvimento adiciona sobrecarga do React DevTools, o Strict Mode renderiza duas vezes e o hot module replacement causa atrasos. Vitals medidos em dev são 2-5x piores do que em produção. Correção: Sempre meça em builds de produção: npm run build && npm start, ou use o Lighthouse com URLs de produção.
Dados de laboratório vs. dados de campo - Pontuações do Lighthouse (dados de laboratório) podem diferir significativamente das métricas de usuários reais (dados de campo) devido a condições de rede, capacidades do dispositivo e distribuição geográfica. Correção: Use ambos: Lighthouse para depuração, Chrome UX Report (CrUX) ou web-vitals para dados de usuários reais.
O elemento LCP não é o que você espera - O elemento LCP pode ser uma imagem de fundo, um pôster de vídeo ou um bloco de texto - nem sempre a imagem principal. Correção: Abra o Lighthouse, expanda o diagnóstico LCP e verifique qual elemento é identificado. Otimize esse elemento específico.
INP não é apenas o primeiro clique - INP mede a pior interação em toda a sessão da página (no percentil 98). Um primeiro clique rápido, mas uma operação de filtro lenta, ainda produz uma pontuação INP ruim. Correção: Perfis de todos os caminhos interativos, não apenas o carregamento inicial da página.
CLS medido em toda a sessão - CLS acumula durante toda a sessão da página, não apenas no carregamento inicial. Um pop-up que aparece 30 segundos após o carregamento ainda contribui para o CLS. Correção: Reserve espaço para todo o conteúdo dinâmico, incluindo modais, toasts, banners e seções carregadas preguiçosamente.
O cache mascara problemas de TTFB - Visitas repetidas com recursos em cache mostram excelente TTFB, escondendo problemas que os visitantes de primeira viagem experimentam. Correção: Teste com o cache desabilitado (DevTools Network -> Disable cache) e de diferentes localizações geográficas.
| Abordagem | Compromisso |
|---|---|
| Lighthouse | Teste de laboratório; repetível, não reflete usuários reais |
| PageSpeed Insights | Usa dados de campo do CrUX; limitado a páginas com tráfego suficiente |
Biblioteca web-vitals | Medição de usuários reais; requer infraestrutura de análise |
| Chrome UX Report (CrUX) | Dados de campo de usuários do Chrome; janela de rolagem de 28 dias |
| Vercel Speed Insights | Automático para implantações Vercel; específico do fornecedor |
| New Relic ou Datadog RUM | Monitoramento empresarial; custo, configuração complexa |
useEffect seja executado.useTransition marca a atualização de estado como não urgente, para que o React mantenha a UI responsiva à entrada do usuário.isPending) em vez de uma UI congelada.width e height explícitos causam mudança de layout porque o navegador não pode reservar espaço até que a imagem carregue.next/image requer dimensões, reserva automaticamente a proporção correta e evita mudanças.sendBeacon é confiável durante o descarregamento da página (por exemplo, quando o usuário navega para longe).fetch regular pode ser cancelado pelo navegador quando a página é descarregada.fetch com keepalive: true como fallback quando sendBeacon não estiver disponível.npm run build && npm start.web-vitals) vêm de sessões de usuários reais com dispositivos e redes variadas.import type { CLSMetric, INPMetric, LCPMetric } from "web-vitals";
function handleMetric(metric: CLSMetric | INPMetric | LCPMetric) {
// metric.rating é tipado como "good" | "needs-improvement" | "poor"
console.log(metric.name, metric.value, metric.rating);
}NextWebVitalsMetric de next/web-vitals.rating é "good" | "needs-improvement" | "poor".name é uma união de nomes de métricas como "LCP", "INP", "CLS", etc.priority desabilita o lazy loading e adiciona uma tag <link rel="preload">.priority.useTransition envolve uma atualização de estado para marcá-la como não urgente.useDeferredValue adia o consumo de um valor, mostrando dados desatualizados enquanto o React renderiza novamente.useDeferredValue é melhor quando você não controla a atualização de estado.Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥