wandres.dev
PROTOCOLO Y AUTOMATIZACIÓN · CDP y Puppeteer

Automatizar auditorías de rendimiento en integración continua

Montar una comprobación de rendimiento que no genere falsos positivos: qué medir, con qué umbrales, cuántas repeticiones, y cómo presentar el resultado para que alguien lo lea.

⏱ 19 min

Una comprobación de rendimiento en integración continua es fácil de montar y difícil de que sobreviva: la mayoría se desactivan en menos de dos meses porque fallan por ruido, porque nadie sabe qué hacer cuando fallan, o porque miden algo que a nadie le importa. Montar una que dure exige tomar cuatro decisiones bien: qué se mide, con qué presupuesto, con cuánta repetición, y cómo se comunica el resultado.

🎯 Al terminar esta lección sabrás
  • Elegir qué métricas comprobar automáticamente y cuáles no.
  • Fijar umbrales a partir de la variabilidad medida y no de deseos.
  • Montar la comprobación con repeticiones y comparación de medianas.
  • Presentar el resultado de forma que produzca una acción.

Qué medir y qué no

No todas las métricas son igual de buenas candidatas para una comprobación automática, y la diferencia está en su variabilidad.

Magnitud ¿Comprobar automáticamente? Por qué
Tamaño de los paquetes Sí, con umbral estricto Determinista, cero ruido
Número de peticiones Sí, con umbral estricto Determinista
Puntuaciones de accesibilidad y buenas prácticas Sí, con umbral estricto Muy poca variabilidad
Tiempo total de bloqueo Sí, con umbral holgado Variable, pero el que más correlaciona
Pintura de contenido mayor Sí, con umbral holgado Variable
Desplazamiento de disposición Sí, con umbral holgado Variable y a veces discreto
Puntuación agregada de rendimiento No como criterio de fallo Es una media ponderada: oculta qué cambió
Tiempos absolutos de interacción No en laboratorio Demasiado ruido

La fila más importante es la del tamaño. Es la única magnitud completamente determinista de la lista, no tiene ruido, se calcula en segundos, y correlaciona bastante bien con el trabajo de arranque. Si solo puedes tener una comprobación automática, que sea esa.

Y la fila de la puntuación agregada merece explicación: como criterio de fallo es mala porque combina cinco cosas, y una caída de cuatro puntos puede ser cualquiera de ellas o una compensación entre varias. Es útil como dato informativo y mala como condición.

⚠️
Cuidado

Las comprobaciones de rendimiento en el mismo servidor que ejecuta el resto de las tareas comparten CPU con compilaciones y pruebas en paralelo, y esa competencia introduce una variabilidad enorme. Si no puedes aislar el ejecutor, la única salida sensata es asumir mucho ruido y usar umbrales muy holgados, o limitarte a las magnitudes deterministas.

Fijar umbrales

El error que mata estas comprobaciones es poner el umbral en el valor deseado. El umbral tiene que salir de dos datos: la variabilidad medida y el presupuesto acordado.

Paso uno: mide la variabilidad. Ejecuta la comprobación diez veces sobre el mismo commit, sin cambiar nada, y mira la dispersión. Ese es el ruido de tu entorno.

Paso dos: el umbral de fallo tiene que estar claramente por encima del ruido. Si tus mediciones varían un quince por ciento entre ejecuciones idénticas, un umbral que falle ante un aumento del diez por ciento va a fallar aleatoriamente.

Paso tres: separa aviso de fallo. Un aviso ante una desviación pequeña informa sin bloquear; un fallo ante una desviación grande bloquea porque hay algo real.

Paso cuatro: compara contra la rama base, no contra un valor absoluto. Un umbral absoluto se queda obsoleto y produce discusiones sobre si subirlo. Una comparación relativa —“este cambio empeora la métrica un veinte por ciento respecto a la rama principal”— es siempre pertinente y no hay que mantenerla.

// Comprobacion de rendimiento con repeticiones, medianas y comparacion
import { launch } from 'chrome-launcher';
import lighthouse from 'lighthouse';

const PRESUPUESTO = {
  'total-blocking-time': { aviso: 200, fallo: 400 },
  'largest-contentful-paint': { aviso: 2500, fallo: 4000 },
  'cumulative-layout-shift': { aviso: 0.1, fallo: 0.25 }
};

async function auditar(url, repeticiones = 5) {
  const chrome = await launch({ chromeFlags: ['--headless=new', '--no-sandbox'] });
  const ejecuciones = [];

  try {
    for (let i = 0; i < repeticiones; i++) {
      const { lhr } = await lighthouse(url, {
        port: chrome.port,
        output: 'json',
        logLevel: 'error',
        onlyCategories: ['performance']
      });
      ejecuciones.push({
        puntuacion: Math.round(lhr.categories.performance.score * 100),
        metricas: Object.fromEntries(
          Object.keys(PRESUPUESTO).map(k => [k, lhr.audits[k].numericValue])
        )
      });
      console.log(`  ejecucion ${i + 1}/${repeticiones}: ${ejecuciones.at(-1).puntuacion}`);
    }
  } finally {
    await chrome.kill();
  }

  const mediana = (valores) => {
    const o = valores.slice().sort((a, b) => a - b);
    return o[Math.floor(o.length / 2)];
  };

  const resultado = {
    url,
    puntuacionMediana: mediana(ejecuciones.map(e => e.puntuacion)),
    metricas: Object.fromEntries(
      Object.keys(PRESUPUESTO).map(k => [k, mediana(ejecuciones.map(e => e.metricas[k]))])
    ),
    dispersion: Object.fromEntries(
      Object.keys(PRESUPUESTO).map(k => {
        const vals = ejecuciones.map(e => e.metricas[k]);
        const med = mediana(vals);
        return [k, med ? +(100 * (Math.max(...vals) - Math.min(...vals)) / med).toFixed(1) : 0];
      })
    )
  };
  return resultado;
}

function evaluar(resultado) {
  const problemas = [];
  for (const [metrica, limites] of Object.entries(PRESUPUESTO)) {
    const valor = resultado.metricas[metrica];
    const ruido = resultado.dispersion[metrica];
    if (ruido > 25) {
      console.warn(`AVISO: ${metrica} tiene un ${ruido}% de dispersion. El entorno es ruidoso.`);
    }
    if (valor > limites.fallo) problemas.push({ metrica, valor: Math.round(valor), nivel: 'FALLO', limite: limites.fallo });
    else if (valor > limites.aviso) problemas.push({ metrica, valor: Math.round(valor), nivel: 'aviso', limite: limites.aviso });
  }
  console.table(problemas.length ? problemas : [{ estado: 'todo dentro de presupuesto' }]);
  return problemas.some(p => p.nivel === 'FALLO');
}

const resultado = await auditar(process.env.URL_A_AUDITAR ?? 'https://example.com', 5);
console.log('Puntuacion mediana:', resultado.puntuacionMediana);
console.table([resultado.metricas]);
if (evaluar(resultado)) process.exit(1);

La comprobación de dispersión que emite un aviso cuando el ruido supera cierto porcentaje es el detalle que hace honesta esta configuración: si el entorno es demasiado ruidoso, el resultado lo dice en lugar de fingir precisión.

La comprobación determinista que no puede fallar por ruido

Junto a la de rendimiento, una comprobación de tamaño da valor inmediato y no tiene ninguno de sus problemas.

// Presupuesto de tamaño por punto de entrada. Determinista y rapido.
import { readFile, readdir, stat } from 'node:fs/promises';
import { gzipSync } from 'node:zlib';
import { join } from 'node:path';

const PRESUPUESTOS_KB = {
  'entrada-principal': 180,
  'entrada-panel': 120,
  'estilos': 60
};

async function medirDirectorio(directorio) {
  const ficheros = await readdir(directorio, { recursive: true });
  const medidas = [];
  for (const f of ficheros) {
    if (!/\.(js|css)$/.test(f)) continue;
    const ruta = join(directorio, f);
    if (!(await stat(ruta)).isFile()) continue;
    const contenido = await readFile(ruta);
    medidas.push({
      fichero: f,
      brutoKB: +(contenido.length / 1024).toFixed(1),
      comprimidoKB: +(gzipSync(contenido).length / 1024).toFixed(1)
    });
  }
  return medidas.sort((a, b) => b.comprimidoKB - a.comprimidoKB);
}

const medidas = await medirDirectorio('./dist');
console.table(medidas.slice(0, 25));

let falla = false;
for (const [prefijo, limite] of Object.entries(PRESUPUESTOS_KB)) {
  const total = medidas
    .filter(m => m.fichero.includes(prefijo))
    .reduce((s, m) => s + m.comprimidoKB, 0);
  const estado = total > limite ? 'EXCEDE' : 'ok';
  console.log(`${prefijo}: ${total.toFixed(1)} KB / ${limite} KB -> ${estado}`);
  if (total > limite) falla = true;
}
if (falla) process.exit(1);

Esa comprobación tarda segundos, no necesita navegador, no tiene ruido, y detecta la causa más común de regresión de rendimiento: alguien añadió una dependencia grande sin darse cuenta.

Presentar el resultado

Una comprobación que falla y nadie sabe qué hacer con ella se acaba desactivando. Tres reglas para que produzca una acción.

El mensaje tiene que decir qué cambió y cuánto, no solo que se superó un umbral. “El paquete principal ha pasado de ciento sesenta a doscientos diez kilobytes” es accionable; “presupuesto excedido” no lo es.

El resultado tiene que estar donde se toma la decisión, es decir, como comentario en la propuesta de cambio y no enterrado en un registro de ejecución.

Tiene que haber una vía de excepción documentada. A veces un aumento está justificado, y si no hay forma de aprobarlo explícitamente, el equipo acabará desactivando la comprobación entera. Una etiqueta que permita saltarse el presupuesto, dejando constancia, es lo que evita que se desactive.

Una comprobación automática de rendimiento no impide que la aplicación sea lenta: impide que empeore sin que nadie lo sepa

Conviene ser preciso sobre qué compra este trabajo, porque las expectativas equivocadas son la razón de que se abandone. Estas comprobaciones no hacen rápida una aplicación y no encuentran problemas de rendimiento: encuentran cambios. Son un mecanismo de conservación, no de mejora. Si tu aplicación es lenta hoy, mañana seguirá siendo exactamente igual de lenta con todas las comprobaciones en verde, porque lo que garantizan es que el estado actual no empeore. Esa es una función valiosa y limitada, y confundirla con una función de mejora produce dos decepciones: la de quien esperaba que la métrica subiera sola, y la de quien creía que con esto ya no hacía falta perfilar. La forma correcta de encajar esto en el trabajo de un equipo es entender que hay dos actividades distintas con herramientas distintas y cadencias distintas. La primera es la conservación: automática, continua, barata, ejecutada en cada cambio, con presupuestos y comparaciones. Su métrica de éxito es que nadie tenga que pensar en ella. La segunda es la mejora: manual, dirigida, cara, ejecutada de vez en cuando por alguien que investiga con un perfil abierto y las preguntas de dominio en la cabeza. Su métrica de éxito son saltos grandes en cosas concretas. Ninguna de las dos sustituye a la otra, y el modo de fallo más común en los equipos es tener solo la primera y creer que con eso el rendimiento está atendido, porque los indicadores están en verde. Un producto puede pasar años con todas sus comprobaciones en verde y siendo cada vez más lento en lo que de verdad importa, si nadie mide nunca las interacciones que la automatización no cubre. Por eso la recomendación con la que cierra este nivel es doble: monta la conservación, que es barata y evita la deriva; y reserva tiempo explícito, en el calendario, para la mejora, porque esa nunca ocurre sola y ninguna herramienta la va a pedir por ti.