Next.js: Análise de Performance & Monitoramento
Última atualização: 2026-08-26
Performance é um atributo-chave — em uma era da internet onde um atraso de 100 milissegundos pode resultar em uma perda de 7% dos usuários, a otimização de performance é uma métrica central da competitividade de um produto.
1. O Que Você Vai Aprender
- Integrar o Lighthouse CI e definir limites de performance (lighthouserc.js)
- Usar @next/bundle-analyzer para visualizar o tamanho do bundle JavaScript
- Coletar dados reais de Core Web Vitals usando a biblioteca Web Vitals
- Integrar @vercel/speed-insights e PostHog para rastreamento de performance
- Personalizar rastreamentos OpenTelemetry em /instrumentation.ts
- Identificar e otimizar rotas lentas
2. Uma História Real de uma Engenheira de Performance
(1) Ponto Problemático: Pontuação Lighthouse de 45; os usuários estão indo embora
Diana é engenheira de performance na plataforma TaskFlow. Ela recebeu recentemente um relatório de análise de comportamento do usuário:
| Métrica | Valor Atual | Referência do Setor | Impacto |
|---|---|---|---|
| LCP | 4.8s | < 2.5s | Taxa de rejeição de usuários 45% |
| FID/INP | 320 ms | < 200 ms | A interação parece travada |
| CLS | 0.35 | < 0.1 | Cliques errados causados por mudanças de layout |
| Tamanho do Bundle | 1.2 MB | < 500 KB | Carregamento lento da primeira tela |
Para piorar, ela não conseguia responder às seguintes perguntas:
- O LCP no ambiente de produção subiu ou desceu na última semana?
- Quais páginas são as mais lentas?
- O código recém-lançado introduz alguma degradação de performance?
(2) Soluções para Sistemas de Monitoramento de Performance
Diana estabeleceu um sistema abrangente de monitoramento de performance:
// Monitoramento de Usuário Real (RUM)
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals(metric => {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(metric)
})
})
}
(3) Resultados
| Dimensão | Antes da Otimização | Depois da Otimização |
|---|---|---|
| LCP | 4.8s | 1.2s |
| Tamanho do Bundle | 1.2 MB | 380 KB |
| Pontuação Lighthouse | 45/100 | 92/100 |
| Detecção de Degradação de Performance | Descoberta após implantação | Interceptada durante a fase de PR |
3. Lighthouse CI
O Lighthouse CI executa automaticamente auditorias Lighthouse no pipeline CI/CD e define limites de orçamento de performance.
graph TB
A[Pipeline CI] --> B[Lighthouse CI]
B --> C[Executar Auditoria Lighthouse]
C --> D{Comparando Performance e Orçamento}
D -->|Aprovado| E[Pipeline Continua]
D -->|Falha| F[Pipeline Interrompido]
F --> G[PR Reporta Falha no Comentário]
G --> H[Desenvolvedores Otimizam Localmente]
style A fill:#cce5ff
style C fill:#fff3cd
style D fill:#f8d7da
style E fill:#d4edda
(1) Configuração lighthouserc.js
// lighthouserc.js
module.exports = {
ci: {
collect: {
// Número de vezes coletado (Encontrar a mediana)
numberOfRuns: 3,
// Páginas a Serem Auditadas
url: [
'http://localhost:3000',
'http://localhost:3000/login',
'http://localhost:3000/dashboard',
'http://localhost:3000/projects',
'http://localhost:3000/projects/p1'
],
// Iniciar Servidor de Desenvolvimento Next.js
startServerCommand: 'npm run start -p 3000',
startServerReadyPattern: 'ready started server',
// Simulação de Equipamento
settings: {
preset: 'desktop',
throttling: {
cpuSlowdownMultiplier: 4,
downloadThroughputKbps: 10000,
uploadThroughputKbps: 5000,
rttMs: 40
}
}
},
assert: {
// Controle de Acesso Baseado em Performance
budgets: [
{
path: '/',
resourceSizes: [
{ resourceType: 'total', budget: 500 * 1024 }, // 500KB
{ resourceType: 'script', budget: 200 * 1024 }, // 200KB
{ resourceType: 'image', budget: 150 * 1024 } // 150KB
],
resourceCounts: [
{ resourceType: 'script', budget: 15 },
{ resourceType: 'stylesheet', budget: 5 },
{ resourceType: 'image', budget: 20 }
]
}
],
// Limiar de Pontuação Lighthouse
assertions: {
// Pontuação por Categoria
'categories:performance': ['warn', { minScore: 0.9 }],
'categories:accessibility': ['warn', { minScore: 0.9 }],
'categories:best-practices': ['warn', { minScore: 0.9 }],
'categories:seo': ['warn', { minScore: 0.9 }],
// Core Web Vitals
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 200 }],
// Outras Métricas-Chave
'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
'interactive': ['warn', { maxNumericValue: 3500 }],
'max-potential-fid': ['warn', { maxNumericValue: 100 }],
// Melhores Práticas
'uses-http2': ['error'],
'uses-responsive-images': ['error'],
'offscreen-images': ['error'],
'unused-javascript': ['warn', { maxNumericValue: 50 * 1024 }],
'unused-css-rules': ['warn', { maxNumericValue: 10 * 1024 }],
'uses-optimized-images': ['error'],
'uses-text-compression': ['error'],
'uses-rel-preconnect': ['warn'],
'uses-rel-preload': ['warn'],
'efficient-animated-content': ['warn'],
'total-byte-weight': ['error', { maxNumericValue: 500 * 1024 }]
}
},
upload: {
target: 'temporary-public-storage'
},
server: {
// Permitir links externos (Ambiente CI)
allowStaticServer: true
}
}
}
(2) Integração CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Instalar dependências
run: npm ci
- name: Construir aplicação
run: npm run build
- name: Executar Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }}
▶ Exemplo: Interpretando Relatórios do Lighthouse CI
Resultados do Lighthouse CI (3 execuções, mediana)
URL: http://localhost:3000/dashboard
┌──────────────┬─────────┬─────────┬──────┐
│ Categoria │ Pontuação │ Alvo │ Passou │
├──────────────┼─────────┼─────────┼──────┤
│ Performance │ 92 │ ≥ 90 │ ✅ │
│ Acessibilidade│ 95 │ ≥ 90 │ ✅ │
│ Melhores Práticas│ 100 │ ≥ 90 │ ✅ │
│ SEO │ 100 │ ≥ 90 │ ✅ │
└──────────────┴─────────┴─────────┴──────┘
Core Web Vitals
LCP: 1.423 ms (≤ 2.500 ms) ✅
TBT: 87 ms (≤ 200 ms) ✅
CLS: 0.05 (≤ 0.1) ✅
Orçamentos
KB Total: 382 de 500 KB ✅
Scripts: 15 de 15 ✅
Imagens: 4 de 20 ✅
4. Bundle Analyzer
@next/bundle-analyzer visualiza o tamanho dos módulos JavaScript minificados para ajudar a identificar dependências muito grandes.
graph LR
A[Processo de Build] --> B[Plugin Bundle Analyzer]
B --> C[Gerar HTML treemap]
C --> D[Análise Aberta no Navegador]
D --> E{Identificando Módulos Grandes}
E --> F[tree-shaking Não está funcionando]
E --> G[Dependências Duplicadas]
E --> H[Uma biblioteca muito grande foi carregada]
style A fill:#cce5ff
style C fill:#d4edda
(1) Configurando Bundle Analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: true,
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
defaultSizes: 'gzip'
})
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
productionBrowserSourceMaps: false,
swcMinify: true
}
module.exports = withBundleAnalyzer(nextConfig)
(2) Análise Operacional
{
"scripts": {
"analyze": "ANALYZE=true npm run build",
"analyze:server": "ANALYZE=true npm run build && npx serve .next/analyze"
}
}
▶ Exemplo: Interpretando o Relatório de Análise de Bundle
ANALYZE=true npm run build
Relatório de Bundle — client/gzip
Módulo Tamanho % do Total
├── node_modules 280 KB 73.3%
│ ├── @radix-ui 85 KB 22.3%
│ ├── react-dom 52 KB 13.6%
│ ├── react 18 KB 4.7%
│ ├── date-fns 45 KB 11.8%
│ ├── recharts 32 KB 8.4%
│ └── lodash 28 KB 7.3%
├── components/ 72 KB 18.8%
│ ├── Dashboard.tsx 18 KB 4.7%
│ ├── ProjectList.tsx 12 KB 3.1%
│ ├── TaskCard.tsx 8 KB 2.1%
│ └── ...
├── pages/ 22 KB 5.8%
└── lib/ 8 KB 2.1%
Total: 382 KB (gzip)
(3) Estratégias de Otimização de Bundle
| Estratégia | Método | Economia Esperada |
|---|---|---|
| Import Dinâmico | const Chart = dynamic(() => import('./Chart')) |
-280 KB Carregamento Inicial |
| Tree Shaking | Use imports ESM em vez de CJS | -20% código morto |
| Code Splitting | Divisão automática por rota (padrão Next.js) | -30% JavaScript na primeira tela |
| Substituição de Dependência | date-fns → dayjs (45KB → 6KB) |
-39 KB |
| Lazy Loading | React.lazy + Suspense |
-85 KB Biblioteca de Interação |
5. Monitoramento de Usuário Real Core Web Vitals
O Lighthouse fornece dados de laboratório; apenas o Monitoramento de Usuário Real (RUM) pode refletir a experiência real do usuário.
graph TB
A[Navegador do usuário] --> B[Coleta de Dados Web Vitals]
B --> C{Tipo de Indicador}
C --> D[LCP Renderização de Conteúdo Máximo]
C --> E[INP Interação para a Próxima Pintura]
C --> F[CLS Mudança Cumulativa de Layout]
C --> G[FCP Renderização de Conteúdo Inicial]
C --> H[TTFB Tempo até o Primeiro Byte]
D --> I[Reportar para Serviços de Analytics]
E --> I
F --> I
G --> I
H --> I
I --> J[PostHog / GA4 / Autoconstruído]
J --> K[Dashboard Visualização]
style B fill:#cce5ff
style I fill:#fff3cd
style K fill:#d4edda
(1) Componente de Relatório Web Vitals
// src/components/WebVitals.tsx
'use client'
import { useReportWebVitals } from 'next/web-vitals'
type MetricType = {
id: string
name: string
value: number
rating: 'good' | 'needs-improvement' | 'poor'
delta: number
}
export function WebVitals() {
useReportWebVitals((metric: MetricType) => {
// Reportar para Serviços de Analytics
const body = {
metric_name: metric.name,
metric_value: metric.value,
metric_rating: metric.rating,
metric_delta: metric.delta,
url: window.location.pathname,
user_agent: navigator.userAgent,
device_type: getDeviceType(),
connection: (navigator as any).connection?.effectiveType || 'unknown',
timestamp: new Date().toISOString()
}
// Usar sendBeacon para garantir que os dados sejam reportados mesmo quando a página for descarregada
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', JSON.stringify(body))
} else {
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(body),
keepalive: true
})
}
})
return null
}
function getDeviceType(): string {
const width = window.innerWidth
if (width < 768) return 'mobile'
if (width < 1024) return 'tablet'
return 'desktop'
}
(2) Rota API de Vitals (Armazenamento de Dados)
// src/app/api/vitals/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function POST(request: NextRequest) {
try {
const data = await request.json()
// Salvar no banco de dados
await prisma.webVital.create({
data: {
metricName: data.metric_name,
metricValue: data.metric_value,
metricRating: data.metric_rating,
url: data.url,
deviceType: data.device_type,
connection: data.connection,
userAgent: data.user_agent,
timestamp: new Date(data.timestamp)
}
})
// Se o indicador for ruim, disparar um alerta
if (data.metric_rating === 'poor') {
await checkAlertThresholds(data)
}
return NextResponse.json({ ok: true }, { status: 200 })
} catch (error) {
console.error('Falha ao armazenar web vital:', error)
return NextResponse.json({ ok: false }, { status: 500 })
}
}
async function checkAlertThresholds(metric: any) {
// Olhando para trás 5 minutos, o número de Indicadores ruins do caminho
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000)
const poorCount = await prisma.webVital.count({
where: {
metricName: metric.metric_name,
metricRating: 'poor',
url: metric.url,
timestamp: { gte: fiveMinAgo }
}
})
// Se exceder o limite, Enviar um Alerta
if (poorCount > 10) {
// await sendAlert(`Degradação de performance detectada em ${metric.url}: ${metric.metric_name} = ${metric.metric_value}`)
console.warn(`ALERTA: ${poorCount} ${metric.metric_name} ruins em ${metric.url}`)
}
}
(3) Consulta do Dashboard de Performance
// src/app/dashboard/performance/page.tsx
import { prisma } from '@/lib/prisma'
async function getPerformanceSummary() {
const today = new Date()
today.setHours(0, 0, 0, 0)
const vitals = await prisma.webVital.groupBy({
by: ['metricName'],
where: {
timestamp: { gte: today }
},
_avg: {
metricValue: true
},
_count: true
})
// Cálculo da proporção good / needs-improvement / poor
const ratings = await prisma.webVital.groupBy({
by: ['metricRating'],
where: {
timestamp: { gte: today }
},
_count: true
})
return { vitals, ratings }
}
▶ Exemplo: Interpretando Dados Web Vitals
Core Web Vitals de Hoje (2026-07-06)
Métrica │ P75 │ P95 │ Bom % │ Ruim %
─────────────┼─────────┼─────────┼─────────┼───────
LCP │ 1.234ms │ 3.567ms │ 85.2% │ 5.1%
INP │ 98ms │ 245ms │ 91.3% │ 2.8%
CLS │ 0.05 │ 0.18 │ 88.7% │ 4.2%
FCP │ 823ms │ 1.945ms │ 90.1% │ 3.5%
TTFB │ 345ms │ 890ms │ 92.4% │ 2.1%
Top 5 Rotas Lentas
/dashboard LCP: 3.2s (200 visitas)
/projects/p1/tasks LCP: 2.8s (150 visitas)
/reports LCP: 2.6s (80 visitas)
/settings LCP: 2.1s (60 visitas)
/analytics LCP: 1.9s (120 visitas)
6. Integração PostHog Speed Insights
PostHog é uma plataforma de análise de produtos de código aberto que inclui gravação de sessão integrada, feature flags e Speed Insights.
(1) Integração no Cliente
// src/components/PostHogProvider.tsx
'use client'
import { posthog } from 'posthog-js'
import { PostHogProvider as PHProvider, usePostHog } from 'posthog-js/react'
import { useEffect } from 'react'
import { useReportWebVitals } from 'next/web-vitals'
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
capture_pageview: false,
capture_performance: true, // Captura Automática Web Vitals
loaded: (ph) => {
if (process.env.NODE_ENV === 'development') ph.opt_out_capturing()
}
})
}
export function PostHogWebVitals() {
const posthog = usePostHog()
useReportWebVitals((metric) => {
posthog.capture('$web_vitals', {
$metric_name: metric.name,
$metric_value: metric.value,
$metric_rating: metric.rating,
$pathname: window.location.pathname,
$device: navigator.userAgent
})
})
return null
}
export function PHProvider({ children }: { children: React.ReactNode }) {
return <PHProvider client={posthog}>{children}</PHProvider>
}
(2) Integração no Layout Raiz
// src/app/layout.tsx
import { PHProvider, PostHogWebVitals } from '@/components/PostHogProvider'
import { WebVitals } from '@/components/WebVitals'
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="pt-BR">
<body>
<PHProvider>
{children}
<WebVitals />
<PostHogWebVitals />
</PHProvider>
</body>
</html>
)
}
7. /instrumentation.ts Rastreamento Personalizado
O Next.js 16 suporta o registro de rastreamentos OpenTelemetry via instrumentation.ts para monitorar performance do lado do servidor e rotas lentas.
// src/instrumentation.ts
// ============================================
// Telemetria Personalizada Next.js 16
// ============================================
import { registerOTel } from '@vercel/otel'
export async function register() {
registerOTel({
serviceName: 'taskflow',
attributes: {
'deployment.environment': process.env.NODE_ENV,
'service.version': process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || 'unknown'
}
})
}
(1) Rastreamento de Span Personalizado
// src/lib/tracing.ts
// Rastreamento de Span Personalizado
const SPAN_NAMES = {
DATABASE_QUERY: 'db.query',
EXTERNAL_API: 'http.request',
CACHE_READ: 'cache.read',
CACHE_WRITE: 'cache.write',
RENDER_COMPONENT: 'component.render'
} as const
export async function trace<T>(
spanName: string,
fn: () => Promise<T>,
attributes?: Record<string, string>
): Promise<T> {
// Se OpenTelemetry Indisponível, Executar Diretamente
if (typeof (globalThis as any).performance === 'undefined') {
return fn()
}
const start = performance.now()
try {
const result = await fn()
const duration = performance.now() - start
// Registrado no sistema de log
if (duration > 100) {
console.warn(
`[TRACE] ${spanName} concluído em ${duration.toFixed(2)}ms`,
{ attributes, duration }
)
}
return result
} catch (error) {
const duration = performance.now() - start
console.error(`[TRACE] ${spanName} falhou após ${duration.toFixed(2)}ms`, error)
throw error
}
}
(2) Uso em Server Action
// src/actions/task.ts
'use server'
import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createTask(formData: FormData) {
return trace('server-action.createTask', async () => {
const title = formData.get('title') as string
await trace('db.query.create-task', () =>
prisma.task.create({
data: { title, projectId: formData.get('projectId') as string, status: 'TODO' }
})
)
revalidatePath('/projects')
return { success: true }
})
}
▶ Exemplo: Rastreamento de Componente de Servidor
Saída:
Server action executa e chama revalidatePath() para atualizar o cache da página.
// src/app/projects/page.tsx
import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'
async function getProjects() {
return trace('db.query.list-projects', async () => {
const projects = await prisma.project.findMany({
include: { _count: { select: { tasks: true } } },
orderBy: { updatedAt: 'desc' },
take: 50
})
return projects
})
}
export default async function ProjectsPage() {
const start = performance.now()
const projects = await getProjects()
const fetchTime = performance.now() - start
return (
<div>
<p data-testid="fetch-time">
Dados obtidos em {fetchTime.toFixed(0)}ms ({projects.length} projetos)
</p>
{/* renderizar projetos... */}
</div>
)
}
Saída:
Renderiza a interface do componente ▶ Exemplo: Rastreamento de Componente de Servidor conforme descrito na seção.
8. Identificando e Otimizando Rotas Lentas
(1) Middleware de Log de Rotas Lentas
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const start = Date.now()
const response = NextResponse.next()
// Registrar o tempo gasto após a conclusão da resposta
response.headers.set('X-Response-Time', '0')
// Usar AsyncLocalStorage ou eventos personalizados
process.nextTick(() => {
const duration = Date.now() - start
// Registrar Requisições Lentas
if (duration > 1000) {
console.warn(
`[ROTA LENTA] ${request.method} ${request.nextUrl.pathname} ` +
`levou ${duration}ms`
)
}
})
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}
(2) Tabela de Análise de Rotas Lentas
| Rota | Tempo Médio de Resposta | P95 | Volume de Requisições | Estratégia de Otimização |
|---|---|---|---|---|
/dashboard |
1.234 ms | 3.567 ms | 10.000/dia | Shell estático PPR + divisão Suspense |
/reports |
2.891 ms | 5.200 ms | 500/dia | Adicionar ISR revalidate=300, cache de relatórios |
/api/search |
1.567 ms | 4.100 ms | 8.000/dia | Adicionar cache Redis e paginação |
/projects/[id]/tasks |
892 ms | 2.100 ms | 6.000/dia | Adicionar índices de banco de dados para limitar consultas N+1 |
(3) Checklist de Otimização de Performance
// 1. Import Dinâmico de Componentes Pesados
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="animate-pulse h-64" />,
ssr: false // Para componentes que não precisam de SEO, desabilitar SSR
})
// 2. Otimização de Imagem
import Image from 'next/image'
<Image
src="/hero.webp"
alt="Hero"
width={1200}
height={630}
priority // Adicionar prioridade para Imagem em Destaque
placeholder="blur"
blurDataURL="data:image/webp;base64,..."
/>
// 3. Recuperação Paralela de Dados
const [projects, tasks, members] = await Promise.all([
getProjects(),
getTasks(),
getMembers()
])
// 4. Adicionar Cabeçalhos de Cache
export const revalidate = 300 // ISR 5 minutos
export const dynamic = 'force-static'
// 5. Resposta de Compressão
// next.config.js
compress: true
// 6. Pré-conectar a Origens de Terceiros
<link rel="preconnect" href="https://api.posthog.com" />
9. Exemplo Completo: Dashboard de Monitoramento de Performance
// src/app/performance/page.tsx
// ============================================
// Dashboard de Monitoramento de Performance
// ============================================
import { prisma } from '@/lib/prisma'
// --- 1. Coleta de Dados ---
async function getPerformanceData() {
const today = new Date()
today.setHours(0, 0, 0, 0)
const weekAgo = new Date(today)
weekAgo.setDate(weekAgo.getDate() - 7)
// Indicadores de Hoje
const todayVitals = await prisma.webVital.groupBy({
by: ['metricName'],
where: {
timestamp: { gte: today },
metricName: { in: ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'] }
},
_avg: { metricValue: true },
_count: true
})
// TOP 10 Rotas Lentas
const slowRoutes = await prisma.webVital.groupBy({
by: ['url'],
where: {
timestamp: { gte: weekAgo },
metricRating: 'poor'
},
_count: true,
_avg: { metricValue: true },
orderBy: { _avg: { metricValue: 'desc' } },
take: 10
})
// Distribuição de Equipamentos
const deviceStats = await prisma.webVital.groupBy({
by: ['deviceType'],
where: { timestamp: { gte: today } },
_count: true
})
// Tendências Diárias
const dailyTrend = await prisma.$queryRaw`
SELECT
DATE(timestamp) as day,
AVG(CASE WHEN metric_name = 'LCP' THEN metric_value END) as avg_lcp,
AVG(CASE WHEN metric_name = 'CLS' THEN metric_value END) as avg_cls
FROM web_vitals
WHERE timestamp >= ${weekAgo}
GROUP BY DATE(timestamp)
ORDER BY day ASC
`
return { todayVitals, slowRoutes, deviceStats, dailyTrend }
}
// --- 2. Componente de Cartão de Métrica ---
function MetricCard({ name, value, rating, count }: {
name: string; value: number; rating: string; count: number
}) {
const colorMap = {
good: 'text-green-600',
'needs-improvement': 'text-yellow-600',
poor: 'text-red-600'
}
const formatValue = (metric: string, val: number) => {
if (metric === 'CLS') return val.toFixed(2)
return `${val.toFixed(0)}ms`
}
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-sm font-medium text-gray-500">{name}</h3>
<p className={`text-2xl font-bold ${colorMap[rating as keyof typeof colorMap] || ''}`}>
{formatValue(name, value)}
</p>
<p className="text-xs text-gray-400">{count} amostras hoje</p>
</div>
)
}
// --- 3. Componentes da Página ---
export default async function PerformancePage() {
const data = await getPerformanceData()
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Dashboard de Performance</h1>
{/* CWV de Hoje */}
<div className="grid grid-cols-5 gap-4">
{data.todayVitals.map(v => (
<MetricCard
key={v.metricName}
name={v.metricName}
value={v._avg.metricValue || 0}
rating={
(v._avg.metricValue || 0) < 2500 ? 'good' :
(v._avg.metricValue || 0) < 4000 ? 'needs-improvement' : 'poor'
}
count={v._count}
/>
))}
</div>
{/* Rotas Lentas */}
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-lg font-semibold mb-4">Rotas Lentas (Últimos 7 Dias)</h2>
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-500">
<th className="pb-2">Rota</th>
<th className="pb-2">Resposta Média</th>
<th className="pb-2">Amostras Ruins</th>
</tr>
</thead>
<tbody>
{data.slowRoutes.map(r => (
<tr key={r.url} className="border-t">
<td className="py-2 font-mono">{r.url}</td>
<td className="py-2">{r._avg.metricValue?.toFixed(0)}ms</td>
<td className="py-2 text-red-600">{r._count}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Distribuição de Equipamentos */}
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-lg font-semibold mb-4">Distribuição de Dispositivos</h2>
<div className="flex gap-8">
{data.deviceStats.map(d => (
<div key={d.deviceType}>
<span className="text-2xl font-bold">{d._count}</span>
<span className="text-gray-500 ml-2">{d.deviceType}</span>
</div>
))}
</div>
</div>
</div>
)
}
❓ Perguntas Frequentes
P: Qual é a diferença entre Lighthouse CI e PageSpeed Insights? R: O Lighthouse CI é executado automaticamente dentro de um pipeline CI/CD e permite definir limites para evitar degradação de performance. O PageSpeed Insights é uma ferramenta online fornecida pelo Google baseada em dados reais de usuários CrUX. Os dois se complementam: Lighthouse CI é usado para verificações pré-implantação, enquanto PageSpeed Insights é usado para verificação pós-implantação.
P: O que LCP, INP e CLS no Core Web Vitals representam? R: LCP (Largest Contentful Paint) mede o tempo de renderização do maior elemento de conteúdo e deve ser < 2.5 segundos. INP (Interaction to Next Paint) mede o tempo de resposta da página às interações do usuário e deve ser < 200 ms. CLS (Cumulative Layout Shift) mede a estabilidade do layout da página e deve ser < 0.1. Essas três métricas impactam diretamente os rankings de busca do Google.
P: Qual é a diferença entre "gzip" e "parsed" no relatório do Bundle Analyzer? R: "gzip" refere-se ao tamanho do arquivo após ser comprimido pelo servidor e transmitido (o tamanho real de transferência de rede), enquanto "parsed" refere-se ao tamanho após o navegador descomprimir e analisar o arquivo (que afeta o tempo de processamento do motor JS). Normalmente, você deve focar no tamanho gzip (gargalo de transferência de rede) e no tamanho parsed (gargalo de processamento de CPU). O Next.js habilita compressão gzip por padrão.
P:
useReportWebVitalse@vercel/speed-insightsentram em conflito? R: Não, eles não entram em conflito.useReportWebVitalsé um callback Web Vitals integrado do Next.js que você pode usar para reportar dados para seu próprio serviço de análise.@vercel/speed-insightsé um serviço pago fornecido pela Vercel que coleta e exibe automaticamente dados RUM. Ambos podem ser usados simultaneamente sem interferir um no outro.
P: Qual é a diferença entre
/instrumentation.tsemiddleware.ts? R:instrumentation.tsé executado uma vez quando a aplicação inicia (quando o servidor inicia) e é usado para registrar telemetria global como OpenTelemetry e Sentry.middleware.tsé executado antes de cada requisição e é usado para interceptação de requisições, redirecionamento e injeção de cabeçalhos. Para rastreamento de performance, recomendamos usarinstrumentation.ts(global), envolvido em uma Server Action (lógica de negócios) e middleware (nível de requisição).
P: Por onde devo começar a otimizar rotas lentas? R: Etapas de otimização recomendadas: (1) Use Bundle Analyzer para identificar e dividir bundles grandes; (2) Defina guardiões de performance usando Lighthouse CI; (3) Colete dados RUM para identificar os 5% de usuários mais lentos; (4) Comece otimizando as rotas mais lentas com o maior volume de requisições (usando PPR, ISR e import dinâmico); (5) Otimize consultas de banco de dados (consultas N+1, índices ausentes).
📖 Resumo
- O Lighthouse CI audita automaticamente performance, acessibilidade e melhores práticas em pipelines CI/CD, usando controle baseado em limites para evitar o merge de código degradado
@next/bundle-analyzervisualiza tamanhos de pacotes, identifica falhas de tree-shaking e dependências superdimensionadas- O RUM Core Web Vitals coleta dados reais de usuários (LCP/INP/CLS) via
useReportWebVitalse os reporta ao serviço de análise - PostHog e
@vercel/speed-insightsoferecem dashboards de análise de performance prontos para uso /instrumentation.tsregistra rastreamentos OpenTelemetry e usa funçõestrace()personalizadas para monitorar performance do servidor- Otimize com base na prioridade após identificar rotas lentas: shells estáticos PPR, cache ISR, import dinâmico e recuperação paralela de dados
📝 Exercícios
-
Questão Básica (⭐): Configure
lighthouserc.jspara auditar a página inicial e a página de login, defina orçamentos de performance de LCP < 2.5s e CLS < 0.1, e executelhci autorunlocalmente para verificar se a configuração passa. -
Exercício Avançado (⭐⭐): Integre
useReportWebVitalsem sua aplicação: (1) Crie uma rota API para reportar dados e armazene-os no banco de dados; (2) Adicione o componenteWebVitalsao layout raiz; (3) Crie uma página/performancepara exibir as métricas CWV de hoje e as 10 rotas mais lentas. -
Desafio (⭐⭐⭐): Implemente um dashboard abrangente de monitoramento de performance: (1) Use Bundle Analyzer para analisar o tamanho atual do bundle e reduza o JavaScript da primeira tela em mais de 40% através de carregamento dinâmico; (2) Configure um gate de GitHub Actions do Lighthouse CI para bloquear o merge de pull requests (PRs) se a pontuação de performance cair mais de 5 pontos; (3) Integre o rastreamento automatizado
capture_performancedo PostHog e crie um gráfico de tendência CWV de 7 dias.