Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
# Ejecuta una auditoría de rendimiento completa en 3 pasos:
# 1. Comprobaciones automatizadas (CI/CD)
# - Análisis de paquete: ANALYZE=true npm run build
# - Lighthouse CI: lhci autorun
# - TypeScript + ESLint: npm run lint && npm run type-check
# 2. Comprobaciones manuales (Desarrollador)
# - React DevTools Profiler: Registra interacciones, revisa el gráfico de llamas
# - Chrome Memory tab: Snapshots de heap antes/después de navegación
# - Core Web Vitals: Auditoría de Lighthouse en URL de producción
# 3. Monitoreo (Producción)
# - web-vitals library: Métricas de usuario real a análisis
# - Error tracking: Sentry, Datadog, o New Relic
# - Bundle size tracking: Compara contra presupuesto en cada PRCuándo usarlo: Antes de cada lanzamiento importante, trimestralmente para aplicaciones establecidas, y cuando los usuarios reportan problemas de rendimiento. Usa el flujo de trabajo de CI para detectar regresiones automáticamente en cada solicitud de incorporación.
# .github/workflows/performance.yml - GitHub Actions con puertas de rendimiento
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
# Construye con análisis de paquete
- name: Analyze bundle
run: ANALYZE=true npm run build
env:
NEXT_TELEMETRY_DISABLED: 1
# Verifica presupuestos de paquete
- name: Check client bundle size
run: |
# Obtén el tamaño del paquete de JavaScript del 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"
# Falla si el paquete del cliente excede 300KB
if [ "$CLIENT_SIZE_KB" -gt 300 ]; then
echo "::error::Client bundle (${CLIENT_SIZE_KB}KB) exceeds 300KB budget"
exit 1
fi
# Carga estadísticas de paquete para comparación
- 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
# Ejecuta 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 - Configuración de Lighthouse CI con presupuestos de rendimiento
{
"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 auditoría 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[] = [
// --- RENDERIZADO (1-6) ---
{
id: "R1",
category: "Renderizado",
severity: "CRITICAL",
description: "Sin re-renderizados innecesarios en inactividad (React DevTools Profiler)",
check: () => true, // Comprobación manual
},
{
id: "R2",
category: "Renderizado",
severity: "CRITICAL",
description: "React.memo en elementos de lista costosos con claves estables",
check: () => true,
},
{
id: "R3",
category: "Renderizado",
severity: "HIGH",
description: "Sin objetos/funciones inline pasadas a componentes memoizados",
check: () => true,
},
{
id: "R4",
category: "Renderizado",
severity: "HIGH",
description: "State colocado con componentes que lo usan",
check: () => true,
},
{
id: "R5",
category: "Renderizado",
severity: "MEDIUM",
description: "useTransition para actualizaciones no urgentes (búsqueda, filtros)",
check: () => true,
},
{
id: "R6",
category: "Renderizado",
severity: "MEDIUM",
description: "Virtualización para listas que superan 100 elementos",
check: () => true,
},
// --- COMPONENTES (7-11) ---
{
id: "C1",
category: "Componentes",
severity: "CRITICAL",
description: "Predeterminado a Server Components, uso mínimo de use client boundaries",
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} archivos "use client"`);
return clientFiles < 20; // El umbral depende del tamaño de la aplicación
},
},
{
id: "C2",
category: "Componentes",
severity: "HIGH",
description: "Sin layouts completos marcados como componentes cliente",
check: () => {
try {
execSync('grep -r "use client" app/**/layout.tsx 2>/dev/null');
return false; // Se encontró "use client" en un layout
} catch {
return true; // Sin "use client" en layouts
}
},
},
{
id: "C3",
category: "Componentes",
severity: "HIGH",
description: "Suspense boundaries granulares (no uno por página)",
check: () => true,
},
{
id: "C4",
category: "Componentes",
severity: "MEDIUM",
description: "Error boundaries alrededor de cada Suspense boundary",
check: () => true,
},
{
id: "C5",
category: "Componentes",
severity: "MEDIUM",
description: "Estados de carga skeleton coinciden con dimensiones de layout final",
check: () => true,
},
// --- PAQUETE (12-17) ---
{
id: "B1",
category: "Paquete",
severity: "CRITICAL",
description: "Paquete cliente bajo 150KB comprimido (landing), bajo 300KB (app)",
check: () => {
// Comprobación automatizada en CI
return true;
},
},
{
id: "B2",
category: "Paquete",
severity: "CRITICAL",
description: "Librerías pesadas importadas dinámicamente (gráficos, editores, mapas)",
check: () => true,
},
{
id: "B3",
category: "Paquete",
severity: "HIGH",
description: "Sin moment.js (usa date-fns), sin lodash completo (usa lodash-es o 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(` Dependencias prohibidas encontradas: ${found.join(", ")}`);
return false;
}
return true;
} catch {
return true;
}
},
},
{
id: "B4",
category: "Paquete",
severity: "HIGH",
description: "Tree-shaking: importaciones nombradas, sin re-exports de barrel file para módulos grandes",
check: () => true,
},
{
id: "B5",
category: "Paquete",
severity: "MEDIUM",
description: "Grupos de rutas dividiendo paquetes de marketing y app",
check: () => true,
},
{
id: "B6",
category: "Paquete",
severity: "MEDIUM",
description: "Sin dependencias sin usar en package.json",
check: () => true,
},
// --- OBTENCIÓN DE DATOS (18-23) ---
{
id: "D1",
category: "Obtención de Datos",
severity: "CRITICAL",
description: "Sin cascadas de fetches - usa Promise.all para consultas independientes",
check: () => true,
},
{
id: "D2",
category: "Obtención de Datos",
severity: "CRITICAL",
description: "Estrategia de caché definida: tiempos de revalidación y etiquetas para todos los datos",
check: () => true,
},
{
id: "D3",
category: "Obtención de Datos",
severity: "HIGH",
description: "Sin consultas N+1 - usa Prisma include/select para relaciones",
check: () => true,
},
{
id: "D4",
category: "Obtención de Datos",
severity: "HIGH",
description: "Obtención de datos colocada en el Server Component que la necesita",
check: () => true,
},
{
id: "D5",
category: "Obtención de Datos",
severity: "MEDIUM",
description: "Revalidación activada en todas las rutas de mutación",
check: () => true,
},
{
id: "D6",
category: "Obtención de Datos",
severity: "MEDIUM",
description: "Deduplicación de SWR o TanStack Query para fetches del lado cliente",
check: () => true,
},
// --- ACTIVOS Y CWV (24-28) ---
{
id: "A1",
category: "Activos",
severity: "CRITICAL",
description: "Todas las imágenes usan next/image con width, height y alt",
check: () => {
try {
execSync('grep -r "<img " app/ components/ --include="*.tsx" 2>/dev/null');
console.log(" Encontradas etiquetas <img> sin procesar - usa next/image en su lugar");
return false;
} catch {
return true; // No se encontraron etiquetas img sin procesar
}
},
},
{
id: "A2",
category: "Activos",
severity: "CRITICAL",
description: "Imagen de LCP tiene prop priority, todas las fuentes usan next/font",
check: () => true,
},
{
id: "A3",
category: "Activos",
severity: "HIGH",
description: "LCP bajo 2.5s, INP bajo 200ms, CLS bajo 0.1",
check: () => true, // Verificado por Lighthouse CI
},
{
id: "A4",
category: "Activos",
severity: "HIGH",
description: "Sin solicitudes externas de CDN de fuentes (etiquetas de enlace de 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: "Activos",
severity: "MEDIUM",
description: "Placeholders borrosos para imágenes sobre el pliegue",
check: () => true,
},
// --- MEMORIA Y MONITOREO (29-30) ---
{
id: "M1",
category: "Memoria",
severity: "HIGH",
description: "Todos los hooks useEffect tienen limpieza adecuada (listeners, timers, fetches)",
check: () => true, // Revisión manual
},
{
id: "M2",
category: "Monitoreo",
severity: "MEDIUM",
description: "Reporte de web-vitals a análisis en producción",
check: () => true,
},
];
// Ejecuta comprobaciones automatizadas
async function runAudit() {
console.log("Auditoría de Rendimiento - Lista de Verificación de 30 Puntos\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} (comprobación manual)`);
manual++;
}
}
console.log("\n" + "=".repeat(60));
console.log(`Resultados: ${passed} pasaron, ${failed} fallaron, ${manual} manuales`);
console.log(`Puntuación: ${Math.round((passed / (passed + failed)) * 100)}%`);
if (failed > 0) process.exit(1);
}
runAudit();Lo que esto demuestra:
Lista de verificación rápida de 10 puntos para cada revisión de PR:
## Lista de Verificación de Rendimiento de PR
- [ ] Sin nuevo "use client" sin justificación
- [ ] Imágenes usan next/image con dimensiones
- [ ] Sin nuevas cascadas de fetches (paralelo con Promise.all)
- [ ] Importaciones dinámicas para componentes mayores a 30KB
- [ ] Hooks useEffect tienen funciones de limpieza
- [ ] Sin objetos inline o funciones pasadas a componentes memoizados
- [ ] Nuevos fetches de datos tienen estrategia de caché (revalidate, tags)
- [ ] Sin dependencias prohibidas (moment, lodash, axios)
- [ ] Suspense boundaries alrededor de secciones asincrónicas
- [ ] Sin console.log en código de producciónPresupuesto de paquete en next.config.ts:
// next.config.ts - Límites de paquete a nivel de Webpack
const nextConfig = {
experimental: {
webpackBuildWorker: true,
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.performance = {
maxAssetSize: 300 * 1024, // 300KB por activo
maxEntrypointSize: 300 * 1024,
hints: "error", // Falla la construcción si se excede
};
}
return config;
},
};Integración de 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 /> {/* Monitoreo de CWV de usuario real */}
<Analytics /> {/* Seguimiento de vista de página y eventos personalizados */}
</body>
</html>
);
}AuditItem para consistencia.@lhci/cli.web-vitals proporciona tipos de TypeScript para todos los objetos de métrica.Las puntuaciones de Lighthouse CI varían entre ejecuciones - Las condiciones de red, la carga de CPU y el rendimiento del contenedor afectan los resultados. Solución: Ejecuta al menos 3 iteraciones (numberOfRuns: 3) y usa puntuaciones medianas para aserciones.
La comprobación de tamaño de paquete cuenta todos los chunks, no solo entrada - El total de JavaScript cliente incluye chunks compartidos, runtime de marco de trabajo y código específico de página. Una única página puede cargar solo un subconjunto. Solución: Mide el tamaño de paquete por ruta usando @next/bundle-analyzer treemap, no solo el tamaño total de .next/static/chunks.
El rendimiento de CI difiere de producción - Los runners de CI son infraestructura compartida con rendimiento variable. Las puntuaciones de Lighthouse en CI pueden ser 10-20 puntos más bajas que en producción. Solución: Establece umbrales de CI ligeramente más bajos que objetivos de producción, y valida con mediciones reales de producción.
Las comprobaciones automatizadas pierden problemas en tiempo de ejecución - El tamaño de paquete y Lighthouse detectan problemas de tiempo de carga pero pierden problemas de rendimiento en tiempo de ejecución como fugas de memoria, interacciones lentas y CLS acumulado. Solución: Complementa comprobaciones automatizadas con sesiones periódicas de análisis manual.
Sobre-optimizar para la puntuación de Lighthouse - Técnicas como diferir la carga de todo para jugar con la puntuación pueden perjudicar la experiencia real del usuario. Solución: Prioriza métricas reales del usuario (CrUX, web-vitals) sobre puntuaciones de laboratorio.
| Enfoque | Compensación |
|---|---|
| Lighthouse CI | Pruebas de laboratorio integrales; las puntuaciones varían entre ejecuciones |
| Vercel Speed Insights | Configuración cero para Vercel; específico del proveedor |
| WebPageTest | Análisis detallado de cascada; integración más lenta en CI |
| Calibre o SpeedCurve | Monitoreo continuo; servicios pagos |
| Reporte personalizado de web-vitals | Datos de usuario real; requiere infraestructura de análisis |
| Bundlewatch | Comparación de paquetes a nivel de PR; enfocado solo en tamaño |
.next/static/chunks y falla la construcción si se excede.numberOfRuns: 3) y usa puntuaciones medianas."use client" sin justificaciónnext/image con dimensionesPromise.all)useEffect tienen funciones de limpiezaconst nextConfig = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.performance = {
maxAssetSize: 300 * 1024,
maxEntrypointSize: 300 * 1024,
hints: "error", // Falla la construcción si se excede
};
}
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 buscar <img en archivos .tsx bajo app/ y components/.next/image en su lugar.<img> accidentales sin procesar que pierden optimización, lazy loading y prevención de CLS.Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥