Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
# Execute uma auditoria completa de performance em 3 passos:
# 1. Verificações automatizadas (CI/CD)
# - Análise de bundle: ANALYZE=true npm run build
# - Lighthouse CI: lhci autorun
# - TypeScript + ESLint: npm run lint && npm run type-check
# 2. Verificações manuais (Desenvolvedor)
# - React DevTools Profiler: Grave interações, verifique o gráfico de chama (flame chart)
# - Aba de Memória do Chrome: Snapshots do heap antes/depois da navegação
# - Core Web Vitals: Auditoria Lighthouse no URL de produção
# 3. Monitoramento (Produção)
# - Biblioteca web-vitals: Métricas de usuários reais para análise
# - Rastreamento de erros: Sentry, Datadog ou New Relic
# - Rastreamento de tamanho de bundle: Compare com o orçamento em cada PRQuando usar: Antes de cada lançamento principal, trimestralmente para aplicativos estabelecidos e quando os usuários relatam problemas de performance. Use o fluxo de trabalho de CI para capturar regressões automaticamente em cada pull request.
# .github/workflows/performance.yml - GitHub Actions com portões de performance
name: Performance Audit
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
bundle-analysis:
name: Bundle Size Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
# Build com análise de bundle
- name: Analyze bundle
run: ANALYZE=true npm run build
env:
NEXT_TELEMETRY_DISABLED: 1
# Verifique os orçamentos de bundle
- name: Check client bundle size
run: |
# Obtenha o tamanho do bundle JS do cliente
CLIENT_SIZE=$(find .next/static/chunks -name "*.js" -exec cat {} + | wc -c)
CLIENT_SIZE_KB=$((CLIENT_SIZE / 1024))
echo "Client bundle: ${CLIENT_SIZE_KB}KB"
# Falhe se o bundle do cliente exceder 300KB
if [ "$CLIENT_SIZE_KB" -gt 300 ]; then
echo "::error::Client bundle (${CLIENT_SIZE_KB}KB) exceeds 300KB budget"
exit 1
fi
# Envie estatísticas do bundle para comparação
- name: Upload bundle stats
uses: actions/upload-artifact@v4
with:
name: bundle-stats
path: .next/analyze/
lighthouse:
name: Lighthouse CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run build
env:
NEXT_TELEMETRY_DISABLED: 1
# Execute Lighthouse CI
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v12
with:
configPath: ./lighthouserc.json
uploadArtifacts: true
temporaryPublicStorage: true
type-and-lint:
name: TypeScript & ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run type-check
- run: npm run lint// lighthouserc.json - Configuração do Lighthouse CI com orçamentos de performance
{
"ci": {
"collect": {
"startServerCommand": "npm start",
"startServerReadyPattern": "ready on",
"url": [
"http://localhost:3000/",
"http://localhost:3000/products",
"http://localhost:3000/dashboard"
],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["warn", { "minScore": 0.9 }],
"categories:best-practices": ["warn", { "minScore": 0.9 }],
"first-contentful-paint": ["error", { "maxNumericValue": 1800 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"interactive": ["warn", { "maxNumericValue": 3500 }],
"unused-javascript": ["warn", { "maxLength": 1 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}// scripts/perf-audit.ts - Script de auditoria manual
import { execSync } from "child_process";
interface AuditItem {
id: string;
category: string;
severity: "CRITICAL" | "HIGH" | "MEDIUM";
description: string;
check: () => boolean | Promise<boolean>;
}
const auditItems: AuditItem[] = [
// --- RENDERIZAÇÃO (1-6) ---
{
id: "R1",
category: "Rendering",
severity: "CRITICAL",
description: "Sem re-renderizações desnecessárias em idle (React DevTools Profiler)",
check: () => true, // Verificação manual
},
{
id: "R2",
category: "Rendering",
severity: "CRITICAL",
description: "React.memo em itens de lista caros com chaves estáveis",
check: () => true,
},
{
id: "R3",
category: "Rendering",
severity: "HIGH",
description: "Sem objetos/funções inline passados para filhos memoizados",
check: () => true,
},
{
id: "R4",
category: "Rendering",
severity: "HIGH",
description: "Estado co-localizado com os componentes que o utilizam",
check: () => true,
},
{
id: "R5",
category: "Rendering",
severity: "MEDIUM",
description: "useTransition para atualizações não urgentes (pesquisa, filtros)",
check: () => true,
},
{
id: "R6",
category: "Rendering",
severity: "MEDIUM",
description: "Virtualização para listas que excedem 100 itens",
check: () => true,
},
// --- COMPONENTES (7-11) ---
{
id: "C1",
category: "Components",
severity: "CRITICAL",
description: "Padrão Server Components, uso mínimo de limites de cliente",
check: () => {
const result = execSync(
'grep -r "use client" app/ --include="*.tsx" --include="*.ts" -l | wc -l'
).toString().trim();
const clientFiles = parseInt(result);
console.log(` Encontrados ${clientFiles} arquivos "use client"`);
return clientFiles < 20; // Limite depende do tamanho do app
},
},
{
id: "C2",
category: "Components",
severity: "HIGH",
description: "Nenhum layout inteiro marcado como componente cliente",
check: () => {
try {
execSync('grep -r "use client" app/**/layout.tsx 2>/dev/null');
return false; // Encontrado "use client" em um layout
} catch {
return true; // Nenhum "use client" em layouts
}
},
},
{
id: "C3",
category: "Components",
severity: "HIGH",
description: "Limites granulares de Suspense (não um por página)",
check: () => true,
},
{
id: "C4",
category: "Components",
severity: "MEDIUM",
description: "Limites de erro em torno de cada limite de Suspense",
check: () => true,
},
{
id: "C5",
category: "Components",
severity: "MEDIUM",
description: "Estados de carregamento esquelético correspondem às dimensões finais do layout",
check: () => true,
},
// --- BUNDLE (12-17) ---
{
id: "B1",
category: "Bundle",
severity: "CRITICAL",
description: "Bundle do cliente com menos de 150KB gzipped (landing), menos de 300KB (app)",
check: () => {
// Verificação automatizada em CI
return true;
},
},
{
id: "B2",
category: "Bundle",
severity: "CRITICAL",
description: "Bibliotecas pesadas importadas dinamicamente (gráficos, editores, mapas)",
check: () => true,
},
{
id: "B3",
category: "Bundle",
severity: "HIGH",
description: "Sem moment.js (use date-fns), sem lodash completo (use lodash-es ou nativo)",
check: () => {
try {
const pkg = require("./package.json");
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const banned = ["moment", "lodash"];
const found = banned.filter((d) => d in deps);
if (found.length > 0) {
console.log(` Dependências banidas encontradas: ${found.join(", ")}`);
return false;
}
return true;
} catch {
return true;
}
},
},
{
id: "B4",
category: "Bundle",
severity: "HIGH",
description: "Tree-shaking: importações nomeadas, sem re-exportações de barrel file para módulos grandes",
check: () => true,
},
{
id: "B5",
category: "Bundle",
severity: "MEDIUM",
description: "Grupos de rotas dividindo bundles de marketing e de aplicativo",
check: () => true,
},
{
id: "B6",
category: "Bundle",
severity: "MEDIUM",
description: "Nenhuma dependência não utilizada em package.json",
check: () => true,
},
// --- BUSCA DE DADOS (18-23) ---
{
id: "D1",
category: "Data Fetching",
severity: "CRITICAL",
description: "Sem buscas em cascata - use Promise.all para consultas independentes",
check: () => true,
},
{
id: "D2",
category: "Data Fetching",
severity: "CRITICAL",
description: "Estratégia de cache definida: tempos de revalidação e tags para todos os dados",
check: () => true,
},
{
id: "D3",
category: "Data Fetching",
severity: "HIGH",
description: "Sem consultas N+1 - use Prisma include/select para relações",
check: () => true,
},
{
id: "D4",
category: "Data Fetching",
severity: "HIGH",
description: "Busca de dados co-localizada no Server Component que a necessita",
check: () => true,
},
{
id: "D5",
category: "Data Fetching",
severity: "MEDIUM",
description: "Revalidação acionada em todos os caminhos de mutação",
check: () => true,
},
{
id: "D6",
category: "Data Fetching",
severity: "MEDIUM",
description: "Deduplicação SWR ou TanStack Query para buscas do lado do cliente",
check: () => true,
},
// --- ASSETS & CWV (24-28) ---
{
id: "A1",
category: "Assets",
severity: "CRITICAL",
description: "Todas as imagens usam next/image com largura, altura e alt",
check: () => {
try {
execSync('grep -r "<img " app/ components/ --include="*.tsx" 2>/dev/null');
console.log(" Tags <img> brutas encontradas - use next/image em vez disso");
return false;
} catch {
return true; // Nenhuma tag img bruta encontrada
}
},
},
{
id: "A2",
category: "Assets",
severity: "CRITICAL",
description: "Imagem LCP tem prop priority, todas as fontes usam next/font",
check: () => true,
},
{
id: "A3",
category: "Assets",
severity: "HIGH",
description: "LCP abaixo de 2.5s, INP abaixo de 200ms, CLS abaixo de 0.1",
check: () => true, // Verificado pelo Lighthouse CI
},
{
id: "A4",
category: "Assets",
severity: "HIGH",
description: "Sem requisições de CDN de fontes externas (tags link do Google Fonts)",
check: () => {
try {
execSync('grep -r "fonts.googleapis.com" app/ --include="*.tsx" --include="*.ts" 2>/dev/null');
return false;
} catch {
return true;
}
},
},
{
id: "A5",
category: "Assets",
severity: "MEDIUM",
description: "Placeholders de blur para imagens acima da dobra (above-the-fold)",
check: () => true,
},
// --- MEMÓRIA & MONITORAMENTO (29-30) ---
{
id: "M1",
category: "Memory",
severity: "HIGH",
description: "Todos os hooks useEffect têm limpeza adequada (listeners, timers, fetches)",
check: () => true, // Revisão manual
},
{
id: "M2",
category: "Monitoring",
severity: "MEDIUM",
description: "web-vitals reportando para análise em produção",
check: () => true,
},
];
// Execute verificações automatizadas
async function runAudit() {
console.log("Auditoria de Performance - Checklist de 30 Pontos\n");
console.log("=".repeat(60));
let passed = 0;
let failed = 0;
let manual = 0;
for (const item of auditItems) {
try {
const result = await item.check();
const status = result ? "PASS" : "FAIL";
const icon = result ? "[OK]" : "[!!]";
console.log(`\n${icon} ${item.id} [${item.severity}] ${item.description}`);
if (result) passed++;
else failed++;
} catch {
console.log(`\n[--] ${item.id} [${item.severity}] ${item.description} (verificação manual)`);
manual++;
}
}
console.log("\n" + "=".repeat(60));
console.log(`Resultados: ${passed} aprovados, ${failed} reprovados, ${manual} manuais`);
console.log(`Pontuação: ${Math.round((passed / (passed + failed)) * 100)}%`);
if (failed > 0) process.exit(1);
}
runAudit();O que isso demonstra:
Checklist rápido de 10 pontos para cada revisão de PR:
## Checklist de PR de Performance
- [ ] Nenhum novo "use client" sem justificativa
- [ ] Imagens usam next/image com dimensões
- [ ] Sem novas buscas em cascata (paralelas com Promise.all)
- [ ] Importações dinâmicas para componentes acima de 30KB
- [ ] Hooks useEffect têm funções de limpeza
- [ ] Sem objetos ou funções inline passados para componentes memoizados
- [ ] Novas buscas de dados têm estratégia de cache (revalidate, tags)
- [ ] Sem dependências banidas (moment, lodash, axios)
- [ ] Limites de Suspense em torno de seções assíncronas
- [ ] Sem console.log em código de produçãoOrçamento de bundle em next.config.ts:
// next.config.ts - Limites de bundle no nível do Webpack
const nextConfig = {
experimental: {
webpackBuildWorker: true,
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.performance = {
maxAssetSize: 300 * 1024, // 300KB por asset
maxEntrypointSize: 300 * 1024,
hints: "error", // Falha na compilação se excedido
};
}
return config;
},
};Integração com Vercel Speed Insights:
// app/layout.tsx
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<SpeedInsights /> {/* Monitoramento CWV de usuários reais */}
<Analytics /> {/* Rastreamento de visualizações de página e eventos personalizados */}
</body>
</html>
);
}AuditItem para consistência.@lhci/cli.web-vitals fornece tipos TypeScript para todos os objetos de métrica.Pontuações do Lighthouse CI variam entre execuções - Condições de rede, carga da CPU e performance do contêiner afetam os resultados. Correção: Execute pelo menos 3 iterações (numberOfRuns: 3) e use pontuações medianas para as asserções.
Verificação de tamanho de bundle conta todos os chunks, não apenas a entrada - O JS total do cliente inclui chunks compartilhados, runtime do framework e código específico da página. Uma única página pode carregar apenas um subconjunto. Correção: Meça o tamanho do bundle por rota usando o treemap do @next/bundle-analyzer, não apenas o tamanho total de .next/static/chunks.
Performance da CI difere da produção - Os runners de CI são infraestrutura compartilhada com performance variável. As pontuações do Lighthouse na CI podem ser 10-20 pontos menores que na produção. Correção: Defina limites de CI ligeiramente mais baixos que os alvos de produção e valide com medições reais de produção.
Verificações automatizadas perdem problemas de tempo de execução - Tamanho do bundle e Lighthouse capturam problemas de tempo de carregamento, mas perdem problemas de performance em tempo de execução como vazamentos de memória, interações lentas e CLS acumulado. Correção: Complemente as verificações automatizadas com sessões periódicas de perfilamento manual.
Otimização excessiva para a pontuação Lighthouse - Técnicas como carregar tudo dinamicamente para manipular a pontuação podem prejudicar a experiência real do usuário. Correção: Priorize métricas de usuários reais (CrUX, web-vitals) sobre pontuações de laboratório.
| Abordagem | Contrapartida |
|---|---|
| Lighthouse CI | Testes de laboratório abrangentes; pontuações variam entre execuções |
| Vercel Speed Insights | Zero configuração para Vercel; específico do fornecedor |
| WebPageTest | Análise detalhada de waterfall; integração mais lenta na CI |
| Calibre ou SpeedCurve | Monitoramento contínuo; serviços pagos |
| Relatórios customizados de web-vitals | Dados de usuários reais; requer infraestrutura de análise |
| Bundlewatch | Comparação de bundle em nível de PR; focado apenas no tamanho |
.next/static/chunks e falha na compilação se excedido.numberOfRuns: 3) e usa pontuações medianas."use client" sem justificativanext/image com dimensõesPromise.all)useEffect têm funções de limpezaconst nextConfig = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.performance = {
maxAssetSize: 300 * 1024,
maxEntrypointSize: 300 * 1024,
hints: "error", // Falha na compilação se excedido
};
}
return config;
},
};interface AuditItem {
id: string;
category: string;
severity: "CRITICAL" | "HIGH" | "MEDIUM";
description: string;
check: () => boolean | Promise<boolean>;
}// app/layout.tsx
import { SpeedInsights } from "@vercel/speed-insights/next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}grep para procurar por <img em arquivos .tsx em app/ e components/.next/image em vez disso.<img> brutas acidentais que perdem otimização, lazy loading e prevenção de CLS.Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥