El rendimiento de las aplicaciones frontend se correlaciona directamente con las tasas de conversión, la profundidad de interacción del usuario y la visibilidad en los resultados de búsqueda de Google (con Core Web Vitals actuando como factores de posicionamiento). Sin embargo, la optimización de velocidad suele ser reactiva: los equipos corrigen latencias a posteriori, cuando la regresión ya impactó en producción.
Las auditorías manuales en el navegador sufren de una alta variabilidad: extensiones de navegador, cachés dinámicas, procesos en segundo plano del sistema operativo y diferencias en el hardware de las estaciones de trabajo distorsionan las mediciones. Establecer controles fiables exige integrar auditorías sintéticas automatizadas de rendimiento directamente en el pipeline de integración continua (CI/CD).
La herramienta Lighthouse CI (@lhci/cli) proporciona una infraestructura estandarizada de nivel empresarial:
Lighthouse CI orquesta el ciclo de vida completo, desde la compilación local para producción hasta la extracción de trazas, métricas y validación de aserciones.
+-------------------------------------------------------------------------+
| GitHub Actions Runner (Ubuntu Container) |
+-------------------------------------------------------------------------+
|
v 1. Build & Collect (lhci collect)
+-------------------------------------------------------------------------+
| Next.js 15 / React Production Build Server |
| pnpm build && pnpm start (Port 3000) |
+-------------------------------------------------------------------------+
|
v CDP (Chrome DevTools Protocol)
+-------------------------------------------------------------------------+
| Headless Chrome with Throttling Profile |
| - CPU Slowdown: 4x (Simulación de procesador móvil de gama media) |
| - Network Throttling: Fast 4G (RTT 150ms, Throughput 1.6 Mbps) |
| - Viewport: Mobile (360x640) o Desktop (1366x768) |
| - Number of Runs: 3 (Cálculo de la pasada mediana / Median Run) |
+-------------------------------------------------------------------------+
|
v 2. Raw Trace & Performance Metrics
+-------------------------------------------------------------------------+
| Core Web Vitals Extractor |
| |
| LCP (Largest Contentful Paint) <= 2.5s -> [Pass / Fail] |
| CLS (Cumulative Layout Shift) <= 0.10 -> [Pass / Fail] |
| INP (Interaction to Next Paint) <= 200ms -> [Pass / Fail] |
| TBT (Total Blocking Time) <= 200ms -> [Pass / Fail] |
+-------------------------------------------------------------------------+
|
v 3. Assertions Engine (lhci assert)
+-------------------------------------------------------------------------+
| Resource Budget Validation (budget.json) |
| |
| Total JS Bundle Size <= 180 KB -> [Pass / Fail] |
| Third-party Scripts <= 50 KB -> [Pass / Fail] |
+-------------------------------------------------------------------------+
|
+------------------+------------------+
| |
[PASS] [FAIL]
v v
PR Check: Green PR Check: Red / Block Merge
Métricas dentro de SLA Degradación de LCP en 400ms (> 2.5s)fetchpriority="high" en cabeceras.min-height, aspect-ratio) en contenedores dinámicos antes de la inyección de creatividades asíncronas o fuentes web.lighthouserc.js#El archivo de configuración organiza las tres etapas clave: collect (recolección de datos), assert (evaluación de umbrales) y upload (almacenamiento de informes y artefactos):
module.exports = {
ci: {
collect: {
// Inicia el servidor de producción antes de ejecutar las auditorías
startServerCommand: 'pnpm run start',
startServerReadyPattern: 'ready on http://localhost:3000',
startServerReadyTimeout: 60000,
url: [
'http://localhost:3000/',
'http://localhost:3000/catalog',
'http://localhost:3000/pricing',
],
// 3 ejecuciones para calcular la mediana y filtrar fluctuaciones del runner CI
numberOfRuns: 3,
settings: {
// Perfil de emulación: simulación móvil o de escritorio
preset: 'desktop', // o 'mobile' para simulación con 4x CPU Throttling
throttlingMethod: 'simulate',
// Omitir auditorías irrelevantes para entornos de prueba locales
skipAudits: ['uses-http2', 'is-on-https'],
},
},
assert: {
// Utiliza el conjunto de reglas base recomendado por Lighthouse
preset: 'lighthouse:recommended',
assertions: {
// 1. Core Web Vitals: Umbrales estrictos de producción
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 200 }],
// 2. Puntuaciones globales por categoría (escala 0.0 a 1.0)
'categories:performance': ['error', { minScore: 0.90 }],
'categories:accessibility': ['error', { minScore: 0.95 }],
'categories:best-practices': ['error', { minScore: 0.95 }],
'categories:seo': ['error', { minScore: 0.95 }],
// 3. Presupuestos de recursos (Performance Budgets)
'resource-summary:script:size': ['error', { maxNumericValue: 180000 }], // 180 KB
'resource-summary:total:size': ['error', { maxNumericValue: 600000 }], // 600 KB
'dom-size': ['error', { maxNumericValue: 1200 }],
},
},
upload: {
// Almacenamiento de reportes HTML en servidor público temporal o privado
target: 'temporary-public-storage',
},
},
};budget.json#El control granular de presupuestos restringe el consumo de red clasificando los assets según tipología e integraciones externas:
[
{
"path": "/*",
"timings": [
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "first-contentful-paint",
"budget": 1200
}
],
"resourceSizes": [
{
"resourceType": "script",
"budget": 180
},
{
"resourceType": "stylesheet",
"budget": 45
},
{
"resourceType": "image",
"budget": 250
},
{
"resourceType": "font",
"budget": 80
},
{
"resourceType": "third-party",
"budget": 50
},
{
"resourceType": "total",
"budget": 600
}
],
"resourceCounts": [
{
"resourceType": "third-party",
"budget": 5
},
{
"resourceType": "script",
"budget": 10
}
]
}
]La métrica INP evalúa la fluidez frente a interacciones reales. En el código de la aplicación usamos la API nativa PerformanceObserver del navegador para registrar tareas prolongadas y tiempos de respuesta a eventos:
/**
* Inicializa la supervisión de Core Web Vitals y perfila la latencia de interacción
*/
export function registerPerformanceObservers(): void {
if (typeof window === 'undefined' || !('PerformanceObserver' in window)) {
return;
}
try {
// 1. Monitoreo de Largest Contentful Paint (LCP)
const lcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
console.info(`[Web Vitals] LCP: ${lastEntry.startTime.toFixed(2)}ms`, lastEntry);
}
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// 2. Registro de Cumulative Layout Shift (CLS)
let clsValue = 0;
const clsObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const layoutShift = entry as PerformanceEntry & { hadRecentInput?: boolean; value: number };
// Excluir desplazamientos originados por entradas directas recientes del usuario
if (!layoutShift.hadRecentInput) {
clsValue += layoutShift.value;
console.info(`[Web Vitals] Cumulative CLS: ${clsValue.toFixed(4)}`);
}
}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
// 3. Supervisión de Interaction to Next Paint (INP) con Event Timing API
const inpObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const eventEntry = entry as PerformanceEntry & {
duration: number;
processingStart: number;
processingEnd: number;
interactionId?: number;
};
// Registrar interacciones que superen el umbral crítico de 200ms
if (eventEntry.duration > 200) {
console.warn(
`[Web Vitals Slow INP] Event "${eventEntry.name}" took ${eventEntry.duration.toFixed(2)}ms`,
{
inputDelay: eventEntry.processingStart - eventEntry.startTime,
processingTime: eventEntry.processingEnd - eventEntry.processingStart,
presentationDelay: eventEntry.duration - (eventEntry.processingEnd - eventEntry.startTime),
}
);
}
}
});
inpObserver.observe({ type: 'event', buffered: true, durationThreshold: 40 });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown performance observer error';
console.error(`Failed to register PerformanceObserver: ${message}`);
}
}performance-gate.yml#El flujo compila la versión de producción optimizada, ejecuta @lhci/cli y publica el veredicto en las comprobaciones del Pull Request:
name: Performance Quality Gate (Lighthouse CI)
on:
pull_request:
branches: [main]
jobs:
lhci:
name: Lighthouse CI Web Vitals Audit
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Next.js production bundle
run: pnpm run build
- name: Run Lighthouse CI
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
run: |
pnpm exec lhci autorun
- name: Post LHCI status comment
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = './.lighthouseci/manifest.json';
if (fs.existsSync(path)) {
const manifest = JSON.parse(fs.readFileSync(path, 'utf8'));
console.log('Lighthouse CI runs completed:', manifest.length);
}Auditar entornos con next dev o Vite invalida los resultados: la falta de minificación, el árbol de código sin podar (Tree-Shaking deshabilitado), sourcemaps voluminosos y los checks de React Dev Mode ralentizan LCP y TBT entre 3 y 5 veces, provocando fallos erróneos e innecesarios.
# ❌ INCORRECTO: Ejecutar lhci contra servidor de desarrollo
startServerCommand: "pnpm run dev"
# ✅ CORRECTO: Auditar estrictamente builds de producción optimizados
startServerCommand: "pnpm run start" # Precedido por pnpm run buildLos runners compartidos en entornos cloud (GitHub Actions, GitLab CI) comparten ciclos de CPU con otros procesos. Una única ejecución (numberOfRuns: 1) es extremadamente susceptible a picos de carga en el host, generando falsos positivos.
Solución: Configurar siempre numberOfRuns: 3 o 5. LHCI selecciona y evalúa automáticamente la pasada mediana estadística.
First Input Delay (FID) solo medía el retardo de la primera interacción del usuario ignorando el tiempo de procesamiento y el repintado posterior. INP evalúa todas las interacciones a lo largo de toda la sesión. Centrarse únicamente en el primer toque deja desprotegida la reactividad global de la app.
Definir URLs objetivo, perfiles de emulación móvil/escritorio y comando de arranque en producción.
Limitar el peso máximo de bundles JS, CSS, tipografías y el total de peticiones de red.
Fijar umbrales estrictos: LCP <= 2.5s, CLS <= 0.1, Total Blocking Time <= 200ms en el bloque assert.
Establecer de 3 a 5 pasadas para filtrar fluctuaciones de hardware en los runners de CI.
Integrar lhci autorun en el flujo de Pull Requests para bloquear merges si se violan los SLAs.