wandres.dev
PERFORMANCE III · Diagnóstico por síntoma

Los dos perfiles que no son picos: repintado continuo y degradación progresiva

Las dos siluetas que se distribuyen en el tiempo en lugar de concentrarse en un pico, por qué se escapan a la inspección normal, y el método de grabación que las hace visibles.

⏱ 18 min

Las cuatro siluetas anteriores tienen algo en común: son picos. Trabajo concentrado que se ve en cuanto se abre el perfil. Quedan dos problemas frecuentes que no producen picos y que por eso sobreviven durante meses en aplicaciones cuyo equipo perfila con regularidad: un repintado que ocurre continuamente en cantidades pequeñas, y una degradación que solo se aprecia comparando el minuto uno con el minuto quince. Los dos exigen un método de grabación distinto.

🎯 Al terminar esta lección sabrás
  • Reconocer la silueta de repintado continuo y localizar el elemento que lo provoca.
  • Explicar por qué un coste distribuido no aparece en un perfil corto.
  • Diseñar la grabación comparativa que hace visible una degradación progresiva.
  • Distinguir una degradación por memoria de una por acumulación de trabajo.

Repintado continuo

La silueta. El hilo principal no está saturado, no hay tareas largas, y sin embargo la pista del compositor y la de rasterización tienen actividad constante. En el resumen del rango, la categoría de pintado se lleva una fracción notable. Los fotogramas se producen a intervalos regulares aunque en la pantalla no parezca cambiar nada.

Por qué se escapa. Ninguna tarea individual supera ningún umbral. Nada se marca. El perfil no tiene nada que llame la atención, y quien lo mira concluye que la página está bien. El coste está repartido en trescientos fotogramas de dos milisegundos cada uno, y esos seiscientos milisegundos se pagan en batería, en calor y en capacidad de respuesta reducida durante todo ese tiempo.

Las causas. Un cursor que parpadea sobre una superficie muy grande. Una animación de un elemento sin capa propia que obliga a repintar toda la región que ocupa. Un elemento fijo sobre contenido que se desplaza, mal aislado. Un vídeo o un lienzo que se redibuja aunque no cambie. Un indicador de carga que sigue animándose después de que la carga haya terminado, oculto detrás de otra cosa.

La comprobación. El resaltado de repintado del panel de renderizado lo hace obvio en dos segundos: las zonas que se repintan se colorean, y si hay color donde no debería cambiar nada, ahí está. Es un caso donde el overlay visual resuelve en un instante lo que el perfil numérico no señala.

Este observador ayuda cuando el overlay no está disponible, por ejemplo en un dispositivo remoto:

// Detecta repintado continuo midiendo la cadencia de fotogramas en reposo
(() => {
  let fotogramas = 0, inicio = performance.now(), ultimo = inicio;
  const huecos = [];

  const tick = t => {
    fotogramas++;
    huecos.push(t - ultimo);
    ultimo = t;
    if (t - inicio < 5000) requestAnimationFrame(tick);
    else {
      const activos = huecos.filter(h => h < 100).length;
      const media = huecos.reduce((s, h) => s + h, 0) / huecos.length;
      console.table([{
        segundos: 5,
        fotogramas,
        fpsMedio: Math.round(1000 / media),
        fotogramasEnReposo: activos,
        veredicto: fotogramas > 200
          ? 'Se estan produciendo fotogramas sin interaccion: hay algo animandose'
          : 'Cadencia baja: la pagina esta en reposo de verdad'
      }]);
    }
  };
  console.log('No toques la pagina durante cinco segundos.');
  requestAnimationFrame(tick);
})();

La interpretación es directa: una página verdaderamente en reposo produce muy pocos fotogramas, porque el navegador no tiene nada que presentar. Una que produce sesenta por segundo sin que nadie la toque tiene algo animándose, y eso es un coste continuo que alguien está pagando en batería.

⚠️
Cuidado

Una animación que continúa cuando su elemento está oculto, fuera de la pantalla o en una pestaña de fondo es un desperdicio puro y es extraordinariamente común, porque las animaciones CSS no se detienen solas al ocultar un ancestro con visibility o al desplazarse fuera de la vista. La combinación de un observador de intersección para pausar lo que no se ve y content-visibility para que el navegador se salte el trabajo de lo que está fuera del área visible elimina la mayoría de estos casos.

Degradación progresiva

La silueta. Ninguna, en un perfil aislado. Ese es el problema. La firma solo existe en la comparación entre dos perfiles del mismo escenario tomados con suficiente distancia: el segundo tiene las mismas formas que el primero pero más grandes. Las mismas funciones, las mismas tareas, todo un veinte, un cincuenta o un trescientos por ciento más largo.

Las dos causas y cómo distinguirlas. La degradación progresiva tiene dos orígenes muy distintos que exigen herramientas distintas.

Acumulación de estructuras. Colecciones que crecen y sobre las que se itera: un registro en memoria, una caché sin límite, una lista de mensajes que nunca se poda, suscriptores que se añaden y no se quitan. El trabajo por operación crece porque la estructura sobre la que se opera creció. Se detecta comparando perfiles: la misma función tarda más porque procesa más.

Presión de memoria. El uso de memoria crece, la recolección de basura se ejecuta más veces y cada vez más cara, y todo lo demás se ralentiza por competencia. Se detecta porque en el perfil aparecen bloques de recolección cada vez más frecuentes y largos, y porque el gráfico de uso de memoria del perfil sube en escalera sin volver a bajar.

La distinción importa porque los arreglos no se parecen: la primera se arregla podando estructuras; la segunda es una fuga y se caza con herramientas de memoria.

El método de grabación comparativa

Este es el aporte principal de esta lección, porque es el procedimiento que la mayoría de la gente no aplica nunca.

Paso uno: define el ciclo. Una secuencia corta de acciones que representa el uso real y que se puede repetir idénticamente. Abrir un elemento, mirar un detalle, volver. Debe durar segundos, no minutos.

Paso dos: graba el ciclo en frío. Página recién cargada. Guarda el perfil con un nombre que incluya el número de ciclo.

Paso tres: repite el ciclo muchas veces sin grabar. Cincuenta, cien, las que hagan falta. Automatízalo si es posible, porque hacerlo a mano es tedioso y poco reproducible.

Paso cuatro: graba el mismo ciclo otra vez. Exactamente el mismo, en las mismas condiciones.

Paso cinco: compara. No los números totales, que pueden variar por ruido, sino las mismas funciones en la vista de abajo arriba de los dos perfiles. Si una función concreta ha multiplicado su tiempo, ya tienes el hilo del que tirar.

// Automatiza el ciclo de repeticion y registra la degradacion por vuelta
async function medirDegradacion(ciclo, vueltas = 50, cada = 10) {
  const medidas = [];
  const usoMemoria = () => performance.memory?.usedJSHeapSize ?? null;

  for (let i = 1; i <= vueltas; i++) {
    const t0 = performance.now();
    await ciclo(i);
    const ms = performance.now() - t0;
    if (i === 1 || i % cada === 0 || i === vueltas) {
      medidas.push({
        vuelta: i,
        ms: Math.round(ms),
        nodos: document.querySelectorAll('*').length,
        escuchasAprox: typeof getEventListeners === 'function' ? '(usa la consola)' : 'n/d',
        heapMB: usoMemoria() ? +(usoMemoria() / 1048576).toFixed(1) : null
      });
    }
  }

  console.table(medidas);
  const primera = medidas[0].ms, ultima = medidas.at(-1).ms;
  const factor = (ultima / primera).toFixed(2);
  console.log('Factor de degradacion:', factor + 'x',
    factor > 1.5 ? '-> hay degradacion, investiga memoria y estructuras' : '-> estable');
  return medidas;
}

// Ejemplo ejecutable: un ciclo que degrada a proposito
const acumulador = [];
await medirDegradacion(async (i) => {
  const nodo = document.createElement('div');
  nodo.textContent = 'ciclo ' + i;
  document.body.append(nodo);
  acumulador.push(nodo);          // referencia que impide liberar
  nodo.remove();                  // se quita del DOM pero no de memoria
  acumulador.forEach(n => n.textContent.length); // trabajo proporcional al acumulado
}, 60, 10);

El ejemplo degrada a propósito y sirve para practicar la lectura: el tiempo por vuelta crece linealmente porque el trabajo recorre una colección que crece, y el número de nodos del documento se mantiene constante mientras la memoria sube, que es exactamente la firma de nodos separados retenidos.

Los problemas que se distribuyen en el tiempo son invisibles para el proceso normal de desarrollo, y por eso llegan a producción siempre

Merece la pena entender por qué estas dos categorías sobreviven tan sistemáticamente, porque la razón no es técnica sino de proceso, y saberla permite corregirlo. El desarrollo ocurre en sesiones cortas sobre estados frescos. Se recarga la página cada pocos minutos porque se acaba de cambiar el código. Se prueba una pantalla en aislamiento. El navegador de desarrollo lleva abierto quince minutos y el perfil que se graba dura ocho segundos. En ese régimen, un coste que se manifiesta a los veinte minutos de uso continuo o que crece un dos por ciento por operación no puede aparecer: no hay ninguna ventana de observación lo bastante larga. Mientras tanto, el usuario real abre la aplicación por la mañana, la deja en una pestaña, y a las cinco de la tarde lleva ochocientas operaciones acumuladas en la misma instancia. Esa asimetría entre el régimen de observación del desarrollador y el régimen de uso del usuario es estructural, no se corrige con más disciplina de perfilado, y explica una experiencia muy común: la aplicación que todo el equipo considera rápida y que un cliente describe como imposible de usar por la tarde. La corrección es de proceso y tiene tres piezas concretas. Una sesión larga deliberada por ciclo de trabajo: una vez cada dos semanas, alguien deja la aplicación abierta durante horas usándola de verdad, con el gráfico de memoria a la vista. Es aburrido y encuentra cosas que ningún otro método encuentra. Un ciclo de repetición automatizado en la batería de pruebas, que ejecute la operación principal cien veces y falle si el tiempo de la última vuelta supera el de la primera por encima de un margen. Es la única forma de convertir esta clase de bug en algo que se detecta antes de salir. Y una métrica de campo que se pueda segmentar por antigüedad de la sesión: si tu instrumentación registra cuánto lleva abierta la pestaña junto a cada medición de respuesta, la degradación aparece como una correlación evidente en el primer gráfico que dibujes, y sin esa dimensión es literalmente invisible en los agregados, porque las sesiones largas son pocas y su peor comportamiento se diluye en la media de las cortas.