Leer la cascada de un fotograma en el panel de rendimiento
Cómo grabar un perfil que sirva, qué eventos corresponden a cada etapa del pipeline, cómo saber en cuál se va el tiempo, y qué significa que el hilo principal esté vacío y aun así se pierdan fotogramas.
El panel de rendimiento enseña el pipeline entero, etapa por etapa, con sus milisegundos y su recuento de elementos afectados. Con el modelo de las ocho etapas en la cabeza, leerlo deja de ser un ejercicio de interpretación de manchas de colores y pasa a ser una lectura directa: esta banda es el recálculo de estilo, mide catorce milisegundos, afecta a tres mil elementos, ahí está el problema. Lo único que hace falta es saber grabar bien y saber qué nombre tiene cada cosa.
- Grabar un perfil de renderizado con las opciones que hacen falta y ninguna más.
- Asociar cada evento del perfil con su etapa del pipeline.
- Diagnosticar el problema a partir de qué banda es la más ancha.
- Reconocer el caso en que el hilo principal está vacío y aun así se pierden fotogramas.
Grabar un perfil que sirva
Cuatro decisiones antes de pulsar el botón, y las cuatro cambian el resultado.
Estrangula la CPU. Un factor de cuatro o de seis aproxima tu portátil a un teléfono de gama media. Sin estrangular, un problema real de un usuario puede costar dos milisegundos en tu máquina y no se ve. Es la opción que más veces se olvida y la que más cambia el diagnóstico.
Activa las capturas de pantalla. Cuestan un poco de tiempo de grabación y a cambio permiten correlacionar un fotograma concreto con lo que se veía. Sin ellas, encontrar el fotograma que te interesa dentro de una grabación de ocho segundos es adivinar.
Graba poco. Tres segundos alrededor de la interacción concreta. Una grabación de treinta segundos es imposible de leer y además el propio perfilador distorsiona más cuanto más tiempo lleva grabando.
Empieza a grabar antes de la interacción y para justo después. El error clásico es grabar, hacer la interacción a los cuatro segundos y buscar después. Lo eficiente es preparar la página en el estado exacto, empezar a grabar, hacer la interacción de inmediato, y parar.
Y una precaución con la caché: si estás midiendo renderizado y no carga, deja la caché activada. Si la desactivas, la mitad del perfil es red y te distrae.
Qué es cada cosa
Los nombres de los eventos han ido cambiando entre versiones del navegador, y la correspondencia con las etapas del pipeline es estable aunque cambien las etiquetas.
| Evento en el perfil | Etapa | Qué está haciendo |
|---|---|---|
| Ejecución de función, temporizadores, eventos | Entrada y JavaScript | Tu código |
| Devolución de fotograma de animación | Entrada y JavaScript | Tu código dentro del ciclo de renderizado |
| Recalcular estilo | Recálculo de estilo | Resolver el estilo calculado de los elementos invalidados |
| Layout | Layout | Calcular tamaños y posiciones |
| Pre-paint | Pre-paint | Actualizar árboles de propiedades y decidir qué repintar |
| Paint | Paint | Generar las listas de dibujo |
| Layerize | Composición | Organizar las capas |
| Commit | Commit | Entregar el resultado al hilo compositor |
Todo eso vive en la pista del hilo principal. Y hay dos pistas más que importan tanto como esa:
La pista de fotogramas. Muestra cada fotograma presentado con su duración, y marca los que se han caído. Es el sitio donde se responde la pregunta “¿estoy perdiendo fotogramas?” sin ambigüedad.
La pista de la GPU. Muestra el trabajo del proceso de gráficos. Cuando esta pista está llena y la del hilo principal vacía, el diagnóstico es completamente distinto, y de eso trata el callout.
Una funcionalidad que merece atención propia: al seleccionar un evento de recálculo de estilo, el panel indica cuántos elementos se han afectado; al seleccionar uno de layout, cuántos nodos necesitaban layout. Esas cifras son más valiosas que los milisegundos, porque no dependen del dispositivo. Un recálculo que toca cuatro mil elementos es un problema en cualquier máquina.
El diagnóstico por banda más ancha
Localizado el fotograma lento, se mira qué banda ocupa más y se aplica la tabla.
Domina el JavaScript. El problema está en tu código, no en el renderizado. Se ataca con las técnicas del hilo principal: hacer menos, trocear, mover fuera del hilo.
Domina el recálculo de estilo. Hay demasiados elementos invalidados. Las causas habituales son un cambio aplicado en un ancestro muy alto que se propaga por herencia, una propiedad personalizada modificada en la raíz, o selectores que obligan a reevaluar más de lo necesario. La corrección es acotar el alcance: aplicar el cambio al subárbol que lo necesita en lugar de al documento.
Domina el layout. Hay demasiados nodos participando en el cálculo de geometría. Puede ser porque el documento es enorme, porque el cambio afecta a un contenedor del que cuelga medio árbol, o porque una estructura de maquetación obliga a recorridos caros. La corrección de fondo es reducir el número de nodos.
Domina la pintura. Se están regenerando listas de dibujo grandes o caras. Las operaciones caras de pintura son las de siempre: sombras grandes, degradados complejos, esquinas redondeadas sobre superficies grandes, y sobre todo desenfoques. La superposición de destellos de pintura enseña el área afectada, y muchas veces la sorpresa es que se está repintando la pantalla entera por un cambio de un elemento pequeño.
Domina el commit o la composición. Hay demasiadas capas. Es el caso que se diagnostica en el panel de capas y no aquí.
Un patrón que conviene reconocer aparte: muchos fotogramas seguidos con la misma forma. Si cada fotograma tiene la misma banda de recálculo de estilo de doce milisegundos, no tienes un fotograma lento: tienes un bucle que invalida el estilo en cada fotograma. Casi siempre es una animación implementada con JavaScript que asigna estilos, o un observador que reacciona a su propio efecto.
El equivalente programático
Todo lo anterior requiere estar delante del navegador. Para dejar la medición puesta y recogerla en producción, la API de fotogramas largos de animación da el mismo desglose, con menos detalle y sin necesidad de nadie mirando:
const fotogramasMalos = [];
new PerformanceObserver((lista) => {
for (const f of lista.getEntries()) {
fotogramasMalos.push({
inicio: Math.round(f.startTime),
total: Math.round(f.duration),
script: Math.round(f.renderStart - f.startTime),
estiloYLayout: Math.round(f.startTime + f.duration - f.styleAndLayoutStart),
bloqueante: Math.round(f.blockingDuration),
culpable: f.scripts?.[0]?.sourceURL?.split('/').pop() ?? null,
});
}
}).observe({ type: 'long-animation-frame', buffered: true });
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden' && fotogramasMalos.length) {
navigator.sendBeacon('/rum-fotogramas', JSON.stringify(fotogramasMalos.slice(0, 30)));
}
});
El array scripts de cada entrada trae la atribución: qué origen, qué URL y qué tipo de invocación provocó el trabajo. Es la información que convierte un agregado de fotogramas lentos en una lista de ficheros a revisar, y es la razón principal para instrumentar esto aunque solo funcione en una parte de tu tráfico.
La disponibilidad está limitada a los navegadores basados en Chromium en el momento de escribir esto. Como fuente de diagnóstico es suficiente; como métrica de producto para presentar, hay que decir de qué navegadores viene.
Hay una situación que deja bloqueada a mucha gente durante horas, y que es completamente diagnosticable en dos minutos si se sabe dónde mirar. El síntoma: el desplazamiento o la animación van a tirones, se pierden fotogramas de forma visible, y la pista del hilo principal está prácticamente vacía. No hay tareas largas, no hay recálculos de estilo, no hay layout. Nada.
La reacción natural es dudar de la medición. La medición está bien: el cuello de botella no está en el hilo principal, y el modelo de las ocho etapas dice exactamente dónde puede estar. Solo quedan dos sitios: el rasterizado y el dibujo. Es decir, la séptima y la octava etapa, las que no aparecen en esa pista.
Las cuatro causas concretas, con su comprobación.
Una: se está rasterizando demasiada superficie. El rasterizado es proporcional a los píxeles, y en un móvil de densidad tres cada píxel de CSS son nueve píxeles reales. Una animación que obliga a rasterizar la pantalla completa en cada fotograma puede saturar los hilos de rasterizado sin tocar el hilo principal. Comprobación: los destellos de pintura enseñan el área que se repinta. Si se ilumina la pantalla entera por un cambio local, ahí está.
Dos: hay efectos caros de GPU. El desenfoque es el sospechoso número uno, y el filtro de fondo el número dos. Los dos se resuelven en la GPU y los dos cuestan proporcionalmente al área y al radio. Un backdrop-filter: blur(24px) sobre una barra que ocupa el ancho de la pantalla se paga en cada fotograma de desplazamiento. Comprobación: la pista de la GPU llena, y la prueba definitiva de quitar el filtro y volver a medir.
Tres: hay demasiadas capas. El compositor recorre todas en cada fotograma. Comprobación: el panel de capas y su recuento.
Cuatro: la memoria de vídeo está agotada y el navegador está descartando y regenerando texturas continuamente. Es el más difícil de ver y tiene una firma característica: empieza a ir mal después de un rato de uso, no desde el principio, y mejora al recargar. Comprobación: el indicador de estadísticas de renderizado de fotogramas, en el panel de renderizado, muestra el consumo de memoria de la GPU en vivo.
El procedimiento completo, en orden, cuando te encuentres en esta situación:
- Activa los destellos de pintura y desplázate. Si se ilumina mucho, es rasterizado.
- Activa los bordes de capa. Si la página parece un mosaico, son capas.
- Abre el panel de capas y mira la memoria total y el motivo de las capas grandes.
- Busca en tu CSS
filter: blurybackdrop-filter, quítalos temporalmente y vuelve a medir. Es la prueba que más veces acierta a la primera. - Deja el indicador de estadísticas de fotogramas abierto y usa la aplicación un par de minutos, vigilando si la memoria de la GPU crece sin parar.
Y una observación que ahorra frustración: este cuadro clínico es mucho más frecuente en móviles que en escritorio, porque la memoria de vídeo es una fracción y la densidad de píxeles es el triple. Un efecto que en tu monitor cuesta dos milisegundos de GPU puede costar dieciocho en un teléfono, y dieciocho no caben en el presupuesto de un fotograma. Si el problema solo se reproduce en móvil y el hilo principal está limpio, empieza por aquí.
Un procedimiento de cinco pasos
Uno: reproduce el problema con la CPU estrangulada y comprueba que se ve en la pista de fotogramas. Si no se ven fotogramas perdidos, no hay problema de renderizado por mucho que lo parezca.
Dos: localiza un fotograma malo representativo, no el peor de todos. El peor suele ser un caso raro; el representativo es el que se repite.
Tres: mira qué banda es la más ancha y aplica la tabla de diagnóstico.
Cuatro: si la pista del hilo principal está vacía, salta al procedimiento de rasterizado y GPU del callout.
Cinco: cambia una sola cosa y vuelve a grabar. El error de método más caro en este panel es tocar tres cosas a la vez, porque después no se sabe cuál funcionó y muchas veces una de las tres empeoró y otra lo compensó.
Graba un perfil de tu interacción más pesada con la CPU estrangulada a seis veces. Anota, del fotograma más representativo, los milisegundos de cada banda y el número de elementos afectados en el recálculo de estilo. Después repite la grabación con la superposición de destellos de pintura activa y captura el área que se ilumina. Con esas dos piezas sabrás si tu problema es de cantidad de elementos o de cantidad de píxeles, que son problemas distintos con soluciones distintas.