wandres.dev
RENDIMIENTO · Medir el jank de verdad

Grabar bien y leer el flame chart de un fotograma perdido

Cómo se toma una grabación que sirva para algo, qué mira uno en cada pista, cómo se abre un fotograma perdido hasta llegar a la causa, y las cuatro firmas visuales que hay que reconocer de un vistazo.

⏱ 21 min

El panel de rendimiento intimida porque enseña todo a la vez, y la reacción habitual es grabar treinta segundos de una sesión completa y mirar la maraña resultante sin saber por dónde empezar. La técnica es la contraria: grabaciones cortísimas, con estrangulamiento, alrededor de una sola interacción, con marcas propias que te digan dónde mirar. Con eso, encontrar la causa de un fotograma perdido son tres clics.

🎯 Al terminar esta lección sabrás
  • Tomar una grabación reproducible que aísle una interacción concreta.
  • Interpretar cada pista de la línea de tiempo y saber cuál responde a qué pregunta.
  • Recorrer un fotograma perdido desde la pista de fotogramas hasta la causa en el hilo principal.
  • Reconocer las cuatro firmas visuales que explican casi todos los problemas de animación.

Grabar bien

Cinco condiciones. Saltarse cualquiera produce una grabación que no se puede interpretar.

Ventana limpia. Perfil nuevo o modo incógnito, sin extensiones. Los bloqueadores de anuncios y las herramientas de desarrollo de frameworks inyectan trabajo en el hilo principal y aparecen en tu flame chart como si fueran tuyos.

Estrangulamiento de CPU. En el engranaje de captura, 4x para aproximar un Android de gama media, 6x para gama baja. Sin esto no vas a reproducir el problema del usuario, y si lo reproduces será tan pequeño que no lo verás.

Grabación corta. Empieza a grabar, haz una interacción, para. Dos o tres segundos. Una grabación de treinta segundos tiene tanta información que el trabajo relevante ocupa dos píxeles de ancho.

Captura de instantáneas activada. Las miniaturas de la parte superior son la forma más rápida de encontrar el instante en el que la animación se ve mal, que es distinto del instante en el que la causa ocurrió.

Marcas propias. Marca el inicio y el fin de lo que te interesa con la API de User Timing y aparecerá dibujado en la pista de tiempos, encima del flame chart. Es la diferencia entre buscar y saber.

performance.mark("apertura-inicio");
abrirPanel();
requestAnimationFrame(() => requestAnimationFrame(() => {
  performance.mark("apertura-fin");
  performance.measure("apertura del panel", "apertura-inicio", "apertura-fin");
}));

El doble requestAnimationFrame es deliberado: el primero se ejecuta antes de que el navegador pinte, el segundo después de que el fotograma se haya compuesto. Es el modismo estándar para medir hasta el píxel, no hasta el final de tu función.

Chromium admite además medidas con metadatos que crean pistas propias en la línea de tiempo, lo que es utilísimo cuando quieres ver el trabajo de tu motor de animación separado del resto:

performance.measure("tween de la tarjeta", {
  start: t0,
  end: performance.now(),
  detail: {
    devtools: { dataType: "track-entry", track: "Animacion", color: "primary" },
  },
});

Las pistas

De arriba abajo, y qué pregunta responde cada una:

Instantáneas. ¿En qué momento se ve mal? Arrastra por encima para ver la película.

Fotogramas. ¿Cuáles se perdieron? Cada rectángulo es un fotograma presentado; los que aparecen marcados en rojo o rayados son fotogramas parcialmente presentados o descartados. Es la pista donde empieza cualquier investigación de jank.

Interacciones. ¿Cuánto tardó la respuesta a un clic o a una pulsación? Cada barra es una interacción con sus tres fases: retardo de entrada, procesamiento y retardo de presentación. Si la barra es larga por el final, el problema no es tu manejador sino lo que vino después.

Tiempos. Tus marcas y medidas. Y las de tu framework, si las emite.

Principal. El flame chart del hilo principal. La anchura es duración, la profundidad es la pila de llamadas. Aquí vive la causa de casi todo.

Rasterizador y GPU. Trabajo fuera del hilo principal. Si el hilo principal está vacío y aquí hay barras densas, tu problema es de píxeles, no de JavaScript.

Red. Descargas. Relevante cuando la animación depende de que algo termine de llegar.

Debajo, el panel de detalle con cuatro pestañas. La que más se usa es De abajo arriba, que agrega el tiempo propio de cada función sin la pila: responde a “qué función consumió más tiempo en total”, que suele ser distinto de “qué barra es más ancha”.

Anatomía de un fotograma perdido

El recorrido es siempre el mismo y conviene hacerlo en este orden.

Uno. Localiza el fotograma en la pista de fotogramas. Busca uno marcado o notablemente más ancho que sus vecinos. Pincha en él: el detalle te dice la duración y si fue descartado o parcialmente presentado.

Dos. Selecciona el rango de ese fotograma. Arrastra en la vista general para acotar la línea de tiempo justo a esos milisegundos. Ahora todo lo que ves ocurrió dentro del fotograma que falló.

Tres. Mira la forma de la pista principal. Aquí es donde se decide el diagnóstico, y hay solo cuatro formas posibles:

  • Una barra amarilla ancha: JavaScript. Alguien ejecutó demasiado código.
  • Un bloque morado ancho de Recalculate Style o Layout: trabajo de layout.
  • Un bloque verde ancho de Paint o Rasterize: píxeles.
  • La pista casi vacía y el fotograma largo igualmente: no es el hilo principal. Mira las pistas de GPU y rasterizador, o sospecha de memoria de vídeo.

Cuatro. Baja a la causa. Si es JavaScript, la pestaña De abajo arriba ordenada por tiempo propio te da la función. Si es layout, busca el triángulo de aviso: el detalle del bloque te dice qué línea de código forzó el recálculo y cuántos elementos se vieron afectados. Si es pintado, activa la instrumentación avanzada de pintado en el engranaje de captura y podrás ver el perfilador de pintado, que muestra las operaciones de dibujo una por una.

Cinco. Comprueba que la causa está donde la ves. Este paso se salta siempre y es el que más tiempo ahorra a largo plazo. El fotograma que se ve mal a menudo no contiene la causa: la causa fue una invalidación provocada dos fotogramas antes cuyo coste se paga ahora. En el detalle de un bloque de layout, Chromium te muestra la primera invalidación con su pila de llamadas, y esa pila suele apuntar a otro sitio.

💡
La pista de interacciones responde una pregunta distinta

Si el problema es que la interfaz responde tarde al tocarla, y no que una animación tartamudee, la pista útil es la de interacciones y la métrica es INP. Son dos problemas distintos con dos herramientas distintas y se confunden constantemente: una animación fluida puede convivir con una respuesta al clic pésima, y al revés.

Las cuatro firmas

Con la práctica se reconocen de un vistazo. Estas son, con lo que significan y lo que hay que hacer.

Escalera descendente de bloques morados finos. Docenas de pares de Recalculate Style y Layout pequeños seguidos, formando un patrón regular. Es layout síncrono forzado en bucle: estás leyendo una medida geométrica después de escribir, dentro de un for. El arreglo es separar las fases: lee todo primero, calcula, escribe después.

// Firma de escalera: una lectura y una escritura por iteracion.
for (const fila of filas) {
  fila.style.height = fila.querySelector(".contenido").offsetHeight + "px";
}

// Sin escalera: todas las lecturas, luego todas las escrituras.
const alturas = filas.map((f) => f.querySelector(".contenido").offsetHeight);
for (const [i, fila] of filas.entries()) {
  fila.style.height = alturas[i] + "px";
}

Un bloque morado enorme de recálculo de estilo. Un solo bloque de varios milisegundos. Pincha: el detalle indica cuántos elementos se vieron afectados. Si son miles, has invalidado demasiado. Las causas habituales son cambiar una clase en html o en body, escribir una custom property heredable en :root dentro de una animación, o un selector con :has() de alcance amplio.

Una franja verde ancha de pintado. Área grande, contenido caro o ambos. Sospecha de box-shadow grandes, filter, backdrop-filter, border-radius con overflow: hidden, degradados a pantalla completa. El perfilador de pintado te dice qué operación domina.

Barras amarillas periódicas del mismo ancho. Trabajo de JavaScript que ocurre en cada fotograma: un requestAnimationFrame que hace demasiado, un manejador de scroll sin acotar, un observador que dispara en cadena. La regularidad es lo que lo delata.

Y una quinta que no es una firma sino su ausencia: la pista principal vacía y fotogramas perdidos igualmente. Cuando esto ocurre, el problema no está en tu código. Las causas reales son casi siempre memoria de GPU agotada por demasiadas capas, texturas enormes por densidad de píxel, o un vídeo o un canvas que satura el rasterizador. Es el tema de la lección siguiente.

El flame chart no muestra la causa: muestra dónde se paga la factura

El error de diagnóstico que más tiempo cuesta, y que se comete incluso con años de experiencia, es tratar el bloque más ancho del flame chart como el culpable. Un bloque ancho es un sitio donde se consumió tiempo, y en un navegador el sitio donde se consume el tiempo y el sitio donde se decidió consumirlo casi nunca coinciden, porque el motor está construido sobre invalidación diferida: cuando escribes una propiedad, el navegador no recalcula nada, solo marca lo afectado como sucio y sigue. El recálculo ocurre después, cuando alguien necesita el resultado —al pintar, o antes si alguien lee una medida—, y es entonces cuando aparece el bloque morado de tres milisegundos. Ese bloque no sabe nada de por qué es tan grande; solo está pagando la suma de todas las invalidaciones que se acumularon desde el último recálculo, y esas pueden venir de cinco sitios distintos de tu código, de tu framework y de una librería de terceros a la vez. Por eso el detalle que hay que buscar siempre es la primera invalidación, que Chromium muestra con su pila de llamadas, y por eso una optimización aplicada al código que está dentro del bloque ancho suele no cambiar nada. La misma lógica explica un fenómeno que desconcierta a todo el mundo la primera vez: quitar una animación aparentemente inocente arregla el tartamudeo de otra que está al otro lado de la pantalla. No hay magia; hay un único hilo principal, una única cola de invalidaciones y un único presupuesto, y el flame chart es el extracto bancario, no el registro de decisiones. Leerlo bien consiste en resistir la tentación de optimizar el gasto más grande y dedicar ese esfuerzo a averiguar quién lo autorizó.