wandres.dev
RENDIMIENTO EN CI · Evitar la regresión

Lighthouse CI: montar la auditoría automática que no miente

Las cuatro fases de la herramienta, la configuración completa comentada, los ajustes del entorno de integración que deciden si los números valen algo, y cómo guardar el histórico.

⏱ 19 min

Automatizar la auditoría de rendimiento es fácil; automatizarla de forma que sus números signifiquen algo requiere entender qué pasa por debajo. La mayoría de las configuraciones que se copian producen una puntuación que sube y baja quince puntos entre ejecuciones idénticas, y un equipo que ve eso durante dos semanas deja de mirarla. Esta lección monta la herramienta, y sobre todo prepara el entorno para que lo que mida sea comparable consigo mismo.

🎯 Al terminar esta lección sabrás
  • Describir las cuatro fases de la herramienta y qué se configura en cada una.
  • Escribir una configuración completa con recogida, aserciones y almacenamiento.
  • Ajustar el entorno de integración continua para reducir la varianza de las medidas.
  • Elegir entre limitación simulada y aplicada según lo que quieras comparar.

Las cuatro fases

La herramienta se organiza en fases independientes que se pueden ejecutar por separado, y entenderlas evita la mayor parte de los problemas de configuración.

Recoger. Arranca el servidor si hace falta, lanza el navegador y ejecuta la auditoría N veces sobre cada URL. Produce ficheros de informe en un directorio temporal. Es la fase cara y la única que mide.

Afirmar. Lee los informes recogidos y comprueba las condiciones que hayas declarado. Es puro cálculo sobre ficheros, no vuelve a medir nada, y es la que decide si el trabajo falla.

Subir. Envía los informes a algún sitio para poder consultarlos: un almacenamiento temporal público, un servidor propio, o el sistema de ficheros.

Servidor. Un componente aparte y opcional que guarda el histórico y permite ver la evolución de cada métrica por rama y por commit. Es lo que convierte la herramienta de un guardián en un instrumento de diagnóstico, y la parte que más se echa de menos cuando no está.

La instalación y el ciclo completo:

npm install --save-dev @lhci/cli

# Ejecuta recoger, afirmar y subir con la configuracion del proyecto.
npx lhci autorun

# O por fases, util para depurar sin volver a medir.
npx lhci collect
npx lhci assert
npx lhci upload

La configuración comentada

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      // Arranca el servidor de produccion y espera a que responda.
      startServerCommand: 'npm run preview',
      startServerReadyPattern: 'Local:',
      startServerReadyTimeout: 60000,

      // Las rutas representativas, no todas. Una por plantilla.
      url: [
        'http://localhost:4321/',
        'http://localhost:4321/catalogo/',
        'http://localhost:4321/producto/ejemplo/',
        'http://localhost:4321/blog/un-articulo/',
      ],

      // Cinco pasadas. Con tres, la mediana sigue siendo muy inestable.
      numberOfRuns: 5,

      settings: {
        preset: 'desktop',              // o se omite para el perfil movil
        onlyCategories: ['performance'],
        // Limitacion simulada: mas rapida y con mucha menos varianza.
        throttlingMethod: 'simulate',
        skipAudits: ['uses-http2'],     // ruido en un servidor local
        chromeFlags: '--no-sandbox --disable-gpu --disable-dev-shm-usage',
      },
    },

    assert: {
      // Mediana de las pasadas. Nunca el peor ni el mejor.
      aggregationMethod: 'median',
      assertions: {
        // Deterministas: pueden fallar el trabajo.
        'total-byte-weight': ['error', { maxNumericValue: 900000 }],
        'unused-javascript': ['error', { maxNumericValue: 150000 }],
        'modern-image-formats': ['error', { minScore: 1 }],
        'uses-text-compression': ['error', { minScore: 1 }],

        // Ruidosas: solo avisan, con umbrales holgados.
        'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
        'total-blocking-time': ['warn', { maxNumericValue: 300 }],
        'cumulative-layout-shift': ['warn', { maxNumericValue: 0.1 }],

        // La puntuacion agregada no se usa como puerta. Ver mas abajo.
        'categories:performance': 'off',
      },
    },

    upload: {
      target: 'lhci',
      serverBaseUrl: process.env.LHCI_SERVER_URL,
      token: process.env.LHCI_TOKEN,
    },
  },
};

Cuatro decisiones de esa configuración que conviene justificar.

Cinco pasadas y no tres. Tres es el valor por defecto y es insuficiente: la mediana de tres muestras de una distribución ruidosa sigue moviéndose mucho. Cinco reduce la incertidumbre de forma perceptible por un coste de tiempo asumible. Por qué exactamente, con la aritmética, es el asunto de la lección sobre variabilidad de este mismo nivel.

Limitación simulada y no aplicada. Es la que ejecuta la carga sin limitar la red y después calcula con un modelo qué habría pasado. Como el modelo es determinista, la varianza de la salida es mucho menor, y en integración continua lo que quieres es comparabilidad entre ejecuciones, no fidelidad al dispositivo. Para lo segundo están los dispositivos reales de la ejecución nocturna.

Rutas representativas, no todas. Una por plantilla. Auditar cuarenta URL de producto multiplica el tiempo y no aporta información nueva, porque todas comparten el mismo código. La excepción legítima es cuando una ruta tiene un perfil de contenido muy distinto —una portada con vídeo frente a una ficha de texto—, y entonces son dos plantillas a efectos de rendimiento aunque compartan componente.

La puntuación agregada desactivada como puerta. Es una media ponderada de varias métricas, lo que significa que una mejora en una puede tapar un empeoramiento en otra, y que un cambio de un punto no te dice qué ha pasado. Es una cifra excelente para una conversación con dirección y una puerta pésima para un trabajo automático. Vigila las métricas individuales.

Ajustar el entorno

Aquí está la diferencia entre una configuración que sirve y una que se acaba desactivando. La máquina que ejecuta la auditoría es un instrumento de medida, y los ejecutores compartidos de los servicios de integración continua son instrumentos muy malos: el procesador es virtual, está compartido con otros trabajos, y su velocidad efectiva varía enormemente entre ejecuciones.

Las medidas que más reducen el ruido, por orden de efecto:

Un ejecutor dedicado. Si el rendimiento importa lo suficiente como para bloquear despliegues, merece la pena una máquina propia. Es la intervención con más efecto de todas, con diferencia, y la que menos se hace.

Nada más en paralelo. Que el trabajo de auditoría no comparta ejecutor con la compilación, las pruebas unitarias ni el análisis estático. La contención de procesador es la principal fuente de varianza.

Fijar la versión del navegador. Un navegador que se actualiza solo introduce escalones en tus series históricas que parecen regresiones tuyas. Fija la versión y actualízala a propósito, anotando la fecha.

Servir desde local y con contenido fijo. Sin red externa, sin terceros, sin CDN, sin datos que cambien. Si tu página real trae contenido de una API, usa datos de prueba fijos: no estás midiendo la API, estás midiendo tu código.

Calentar antes de medir. La primera ejecución paga la compilación del navegador y el llenado de cachés del sistema. La herramienta ya descarta parte de eso, pero una carga previa de descarte ayuda.

Los indicadores en el contenedor merecen una línea aparte porque son causa habitual de fallos crípticos: --no-sandbox hace falta en la mayoría de los contenedores, y --disable-dev-shm-usage evita los cuelgues por memoria compartida pequeña que producen errores que parecen de la página y no lo son.

Y una comprobación de salud que conviene tener y que casi nadie tiene: mide la propia máquina en cada ejecución. Un trabajo que registra cuánto tarda un trabajo de referencia fijo antes de auditar te dice, cuando aparezca una regresión, si el que estaba lento era tu sitio o el ejecutor.

# Antes de auditar: cuanto tarda un trabajo fijo en esta maquina.
node -e '
const t = Date.now();
let s = 0;
for (let i = 0; i < 3e7; i++) s += Math.sqrt(i);
console.log("indice_de_maquina_ms", Date.now() - t);
'

Guarda ese número junto a los resultados. El día que una métrica se dispare, lo primero que hay que mirar es si el índice de máquina también se disparó, y eso resuelve una fracción sorprendentemente grande de las falsas alarmas.

El histórico

La fase de subida a un almacenamiento temporal público sirve para empezar y para revisar un informe puntual desde un enlace en la petición de cambios. Para trabajar de verdad hace falta el servidor propio, porque el valor de estas medidas no está en el número de hoy sino en la serie.

Con el histórico puedes hacer tres cosas que sin él son imposibles: ver si una regresión es un salto o una deriva lenta, saber si el umbral que vas a poner deja fuera los últimos tres meses de valores normales, y responder a la pregunta de qué commit lo rompió sin bisecar a mano.

# Crea el proyecto y devuelve el token de escritura.
npx lhci wizard

Un aviso de retención: los informes completos ocupan bastante y crecen rápido con cinco pasadas por cuatro rutas por commit. Conviene una política explícita —informes completos un mes, métricas agregadas para siempre— antes de que la base de datos crezca sin control.

Una comprobación de rendimiento en integración continua tiene dos usuarios y solo uno de ellos es una máquina: si el humano no entiende el fallo en treinta segundos, la comprobación está muerta

La instrumentación técnica de esto es la parte fácil y la que ocupa toda la documentación. La parte que decide si sigue viva dentro de seis meses no aparece en ninguna guía, y es el diseño de lo que ve la persona cuando falla. Ponte en su situación: son las seis y media de la tarde, tiene un cambio de tres líneas que arregla un error visible para clientes, y una comprobación en rojo que dice que la métrica de bloqueo total ha pasado de 280 a 340 milisegundos. Esa persona tiene exactamente tres opciones. Puede investigar, lo que significa entender qué es esa métrica, si 60 milisegundos son ruido o señal, si su cambio pudo causarlo, y cómo se arregla; una hora larga, en el mejor de los casos, para alguien que probablemente no ha leído nunca sobre esto. Puede pedir que le salten la comprobación, lo que crea el precedente y a la tercera vez ya nadie pregunta. O puede desactivarla, en cuyo caso el trabajo de meses que costó montarla desaparece en un commit. Fíjate en que las tres opciones son racionales desde su punto de vista, y en que la probabilidad de la primera depende casi por completo de cómo esté redactado el mensaje de error. Un mensaje que dice “assertion failed: total-blocking-time expected less than 300, got 340” empuja a la tercera opción. Un mensaje que dice que el paquete de la ruta de catálogo ha crecido 18 kilobytes, que el crecimiento viene de una dependencia concreta importada en un fichero concreto, que la línea que la importa es esta, y que si es intencionado el procedimiento para subir el presupuesto está en este enlace, empuja a la primera, porque el trabajo que pide es de dos minutos y no de una hora. De ahí salen tres reglas de diseño que valen para cualquier comprobación automática de rendimiento y que raramente se aplican. Primera: falla solo sobre cosas que la persona pueda atribuir a su cambio, que en la práctica significa métricas deterministas, y deja las ruidosas como aviso. Segunda: el mensaje de error tiene que contener el diagnóstico, no el síntoma; si tu herramienta no puede decir qué creció, la comprobación no está lista. Y tercera: tiene que existir un camino de excepción documentado, rápido y con caducidad, porque una puerta sin llave se derriba y una con llave se respeta. La medida de éxito de todo este nivel no es cuántas regresiones detectas: es cuántos meses lleva la comprobación activa sin que nadie haya intentado quitarla.

⚔️ Monta la auditoría
  1. Instala la herramienta y consigue una ejecución local con cinco pasadas sobre cuatro rutas representativas.
  2. Ejecuta el mismo commit diez veces sin cambiar nada y anota el rango de cada métrica. Ese rango es tu ruido de fondo.
  3. Añade el índice de máquina antes de cada auditoría y guárdalo con los resultados.
  4. Levanta el servidor de histórico y carga las últimas dos semanas de commits para tener una serie con la que comparar.
  5. Escribe el mensaje que verá quien rompa una aserción y compruébalo con alguien que no haya trabajado en esto.