Lista de Verificación de Auditoría de Rendimiento — Revisión sistemática de 30 puntos con cumplimiento de CI
Receta
# 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.
Ejemplo de Funcionamiento
# .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:
- 30 elementos de auditoría en 6 categorías con clasificaciones de severidad
- Comprobaciones automatizadas de CI: tamaño de paquete, puntuaciones de Lighthouse, TypeScript, ESLint
- Comprobaciones manuales: Profiler, snapshots de memoria, revisión de skeleton
- Flujo de trabajo de GitHub Actions que bloquea PRs que fallan presupuestos de rendimiento
- Lighthouse CI con umbrales numéricos específicos para cada métrica
Exploración Profunda
Cómo Funciona
- Presupuestos de paquete establecen tamaños máximos para paquetes de JavaScript. El flujo de trabajo de CI mide el paquete del cliente después de la construcción y falla si excede el umbral. Las páginas de landing deben orientarse a menos de 150KB comprimido; páginas de app menos de 300KB comprimido.
- Lighthouse CI ejecuta Lighthouse en un entorno de CI, recopilando puntuaciones de rendimiento en múltiples URLs con múltiples ejecuciones para confiabilidad estadística. Las aserciones imponen puntuaciones mínimas y valores máximos de métricas.
- Niveles de severidad priorizan elementos de auditoría. Los elementos CRITICAL causan problemas de rendimiento inmediatamente visibles para el usuario. Los elementos HIGH causan degradación medible. Los elementos MEDIUM son mejores prácticas que previenen problemas futuros.
- Comprobaciones automatizadas vs manuales — Algunos elementos (tamaño de paquete, escaneo de dependencias, reglas de lint) pueden ser completamente automatizados. Otros (análisis de Profiler, detección de fugas de memoria, coincidencia de layout skeleton) requieren criterio humano.
Variaciones
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>
);
}Notas de TypeScript
- Los tipos de script de auditoría se definen con la interfaz
AuditItempara consistencia. - La configuración de Lighthouse CI es JSON, no TypeScript, pero puede validarse con tipos de
@lhci/cli. - La librería
web-vitalsproporciona tipos de TypeScript para todos los objetos de métrica.
Trampas
-
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-analyzertreemap, 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.
Alternativas
| 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 |
FAQs
¿Cuáles son las seis categorías en la auditoría de rendimiento de 30 puntos?
- Renderizado (R1-R6): re-renderizados, memoización, transiciones, virtualización
- Componentes (C1-C5): Server Components, Suspense boundaries, skeletons
- Paquete (B1-B6): tamaño de paquete, importaciones dinámicas, tree-shaking, dependencias
- Obtención de Datos (D1-D6): cascadas, caché, consultas N+1, revalidación
- Activos y CWV (A1-A5): imágenes, fuentes, objetivos de LCP y CLS
- Memoria y Monitoreo (M1-M2): limpieza de efectos, reporte de web-vitals
¿Qué presupuestos de tamaño de paquete debes imponer en CI?
- Páginas de landing: menos de 150KB comprimido de JavaScript cliente.
- Páginas de aplicación: menos de 300KB comprimido de JavaScript cliente.
- Impone con un paso de CI que mida
.next/static/chunksy falla la construcción si se excede.
¿Cómo mejora Lighthouse CI la confiabilidad sobre una única ejecución de Lighthouse?
- Ejecuta múltiples iteraciones (se recomienda
numberOfRuns: 3) y usa puntuaciones medianas. - Las ejecuciones individuales varían debido a carga de CPU, red y rendimiento del contenedor.
- Las aserciones imponen puntuaciones mínimas y valores máximos de métricas por URL.
¿Qué debe cubrir la lista de verificación rápida de 10 puntos de PR?
- Sin nuevo
"use client"sin justificación - Las imágenes usan
next/imagecon dimensiones - Sin cascadas de fetches (usa
Promise.all) - Importaciones dinámicas para componentes mayores a 30KB
- Los hooks
useEffecttienen funciones de limpieza
¿Cómo estableces límites de paquete a nivel de webpack en next.config.ts?
const 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;
},
};Trampa: ¿Por qué difieren las puntuaciones de Lighthouse CI en CI de las puntuaciones de producción?
- Los runners de CI son infraestructura compartida con rendimiento variable de CPU y red.
- Las puntuaciones 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.
¿Cuál es la diferencia entre comprobaciones de auditoría automatizadas y manuales?
- Automatizadas: Tamaño de paquete, escaneo de dependencias, reglas de lint, Lighthouse CI -- ejecutado en cada PR.
- Manuales: React DevTools Profiler, snapshots de heap, revisión de layout skeleton -- requieren criterio humano.
- Ambas son necesarias: automatizadas detectan regresiones, manuales detectan problemas en tiempo de ejecución.
Trampa: ¿Por qué puede sobre-optimizar para una puntuación de Lighthouse perjudicar a usuarios reales?
- Técnicas como diferir la carga de todo para jugar con la puntuación pueden retrasar contenido sobre el pliegue.
- Las puntuaciones de laboratorio no reflejan dispositivos reales del usuario y redes.
- Solución: Prioriza métricas reales del usuario (CrUX, web-vitals) sobre puntuaciones de laboratorio.
¿Cómo escribes la interfaz AuditItem para el script de auditoría en TypeScript?
interface AuditItem {
id: string;
category: string;
severity: "CRITICAL" | "HIGH" | "MEDIUM";
description: string;
check: () => boolean | Promise<boolean>;
}¿Cómo integras Vercel Speed Insights para monitoreo automático de CWV?
// 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>
);
}¿Qué nivel de severidad debe desencadenar una corrección inmediata vs una mejora planeada?
- CRITICAL: Corrección inmediata requerida -- causa problemas de rendimiento visibles para el usuario (p. ej., paquete fuera de presupuesto, cascadas de fetches).
- HIGH: Corregir en el sprint actual -- causa degradación medible (p. ej., consultas N+1, optimización de imagen faltante).
- MEDIUM: Planificar para el próximo sprint -- mejores prácticas que previenen problemas futuros (p. ej., dependencias sin usar, Suspense faltante).
¿Cómo funciona la comprobación automatizada de etiquetas img sin procesar en el script de auditoría?
- El script usa
greppara buscar<imgen archivos.tsxbajoapp/ycomponents/. - Si se encuentra, la comprobación falla -- todas las imágenes deben usar
next/imageen su lugar. - Esto detecta etiquetas
<img>accidentales sin procesar que pierden optimización, lazy loading y prevención de CLS.
Relacionado
- Core Web Vitals — Optimización detallada para cada métrica de CWV
- Optimización de Paquete — Estrategias para cumplir presupuestos de tamaño de paquete
- Análisis de Rendimiento — Técnicas de análisis manual para el proceso de auditoría
- Fugas de Memoria — Técnicas de auditoría de memoria y uso de Chrome DevTools