wandres.dev
RENDIMIENTO · Medir el jank de verdad

Diagnosticar por qué una animación cae a 30 fps

El árbol de causas ordenado por frecuencia, cinco casos reales con su firma en el perfil y su arreglo, qué hacer cuando el problema no está en tu código, y la lista de comprobación completa.

⏱ 21 min

Con el presupuesto claro, el flame chart leído, las capas contadas y las tareas largas identificadas, queda juntarlo todo en un procedimiento. Un diagnóstico de jank no es una búsqueda creativa: es un árbol de decisión que se recorre en el mismo orden siempre, y en el que cada rama se descarta con una observación concreta. Esta lección cierra el nivel con ese árbol y con los cinco casos que explican la mayoría de los problemas reales.

🎯 Al terminar esta lección sabrás
  • Recorrer el árbol de causas descartando ramas con observaciones objetivas.
  • Reconocer cinco patrones frecuentes por su firma en el perfil y aplicar el arreglo correcto.
  • Distinguir los problemas causados por código de terceros y por el dispositivo.
  • Ejecutar una lista de comprobación completa antes de dar una animación por buena.

El árbol de causas

flowchart TB
ini[La animacion cae a 30 fps] --> q1{Sobrevive a un bloqueo del hilo principal}
q1 -->|Si| gpu[No es el hilo principal]
q1 -->|No| q2{El hilo principal esta ocupado durante el fotograma}
q2 -->|No| arranque[El problema es el arranque no la animacion]
q2 -->|Si| q3{Domina un bloque de script}
q3 -->|Si| script[Tarea larga o trabajo por fotograma]
q3 -->|No| q4{Domina layout o recalculo de estilo}
q4 -->|Si| layout[Propiedad que dispara layout o invalidacion amplia]
q4 -->|No| pintado[Area de pintado grande o filtro caro]
gpu --> q5{Hay muchas capas o texturas enormes}
q5 -->|Si| capas[Explosion de capas o memoria de video]
q5 -->|No| raster[Rasterizado saturado por densidad de pixel]
style ini fill:#f38ba8,color:#11111b
style gpu fill:#fab387,color:#11111b
style script fill:#f9e2af,color:#11111b
style layout fill:#f9e2af,color:#11111b
style pintado fill:#f9e2af,color:#11111b
style capas fill:#cba6f7,color:#11111b
style raster fill:#cba6f7,color:#11111b
style arranque fill:#89b4fa,color:#11111b

La primera pregunta es la más barata y la que más ramas poda. Bloquea el hilo principal a propósito mientras la animación corre y mira si sigue:

const bloquear = (ms) => { const f = performance.now() + ms; while (performance.now() < f); };
setTimeout(() => bloquear(1200), 300);

Si sigue fluida durante el bloqueo, tu animación está en el compositor y el problema no está en el hilo principal: salta directamente a la rama de capas y rasterizado. Si se congela, sigue por la izquierda. Esa única observación divide el espacio de búsqueda por la mitad en cinco segundos.

Cinco casos reales

Uno. La rejilla que se hunde al pasar el ratón. Síntoma: la página va bien hasta que aparece una rejilla de tarjetas; entonces todo va a tirones, sobre todo en móvil. Firma: el hilo principal está tranquilo. En el panel de capas hay decenas de losas con la razón de composición “solapamiento”. La memoria de GPU en las estadísticas de renderizado es alta y no baja. Causa: will-change: transform en la clase de la tarjeta, permanente, más composición implícita de las insignias y los menús que se solapan. Arreglo: quitar el will-change de la hoja de estilos y aplicarlo desde JavaScript solo durante la interacción, o promocionar únicamente el contenedor. Está desarrollado en Las capas del compositor.

Dos. El desplazamiento que tartamudea. Síntoma: la página se mueve a trompicones al hacer scroll, y solo al hacer scroll. Firma: escalera de bloques morados finos alternando Recalculate Style y Layout, uno por evento de scroll, con el triángulo de aviso de reflujo forzado. Causa: un manejador de scroll que lee getBoundingClientRect o offsetTop y luego escribe estilos, normalmente para una barra que se pega o para un efecto de aparición. Arreglo: sustituir la lectura por IntersectionObserver, o por una animación dirigida por scroll si el navegador la soporta, o —si no queda más remedio— leer todo en un bloque y escribir en otro dentro de un requestAnimationFrame. Y marcar el manejador como { passive: true }.

Tres. El panel que se abre tarde pero se abre bien. Síntoma: pulsas el botón, no pasa nada durante un cuarto de segundo, y entonces el panel se desliza perfectamente. Firma: la pista de interacciones muestra una barra larga con la mayor parte en procesamiento. En el flame chart hay una tarea larga entre el evento y el primer fotograma de la animación. Causa: algo caro se ejecuta en el mismo manejador: hidratación de un componente, una llamada de analítica síncrona, la construcción de todo el contenido del panel. Arreglo: arrancar la animación primero y hacer el trabajo después, cediendo el hilo. La animación no depende del contenido para empezar.

boton.addEventListener("click", async () => {
  panel.classList.add("abierto");     // la animacion arranca ya
  await new Promise((r) => requestAnimationFrame(r));
  await cederElHilo();
  rellenarContenido(panel);           // el trabajo caro, despues
});

Cuatro. El acordeón que va a saltos. Síntoma: el despliegue no es suave, y cuanto más largo el contenido, peor. Firma: bloques morados de layout en cada fotograma, con el número de cajas afectadas creciendo con el tamaño del documento. Causa: se está animando height. Cada fotograma recalcula la geometría de todo lo que viene después en el flujo. Arreglo: interpolate-size: allow-keywords con height: auto si puedes permitírtelo, o el patrón de escalar un contenedor con transform y contrarrestar en el hijo, o grid-template-rows de 0fr a 1fr, que también es layout pero mucho más acotado si el contenedor tiene contain.

Cinco. Va bien en el portátil y fatal en el móvil. Síntoma: nada reproducible en desarrollo. Firma: con estrangulamiento de CPU a 6x, el bloque de rasterizado se dispara. En el dispositivo real, las estadísticas de renderizado muestran memoria de GPU cerca del límite. Causa: densidad de píxel. Una capa a pantalla completa con devicePixelRatio 3 tiene nueve veces los píxeles de una con densidad 1, y el móvil tiene una fracción del ancho de banda de memoria del portátil. Arreglo: reducir el área animada, evitar filter y backdrop-filter sobre superficies grandes, y comprobar que no estás rasterizando capas que no se ven.

Cuando no es tu código

Tres situaciones en las que el diagnóstico correcto es “esto no lo arreglo yo”, y conviene reconocerlas pronto para no perder días.

Extensiones del navegador. Bloqueadores, gestores de contraseñas y herramientas de desarrollo inyectan observadores del DOM que se ejecutan en cada mutación. Un MutationObserver de una extensión sobre un árbol que tu animación modifica puede duplicar el coste de cada fotograma. Reproduce siempre en un perfil limpio antes de investigar.

Etiquetas de terceros. Analítica, mapas de calor, chats de soporte, píxeles publicitarios. Aparecen en el flame chart con su propio sourceURL y suelen ejecutar trabajo periódico. La atribución de long-animation-frame los identifica sin ambigüedad, y eso convierte una discusión en un dato.

El propio dispositivo. Batería baja con modo de ahorro activo, térmicamente limitado, otra pestaña haciendo trabajo pesado. No siempre hay nada que arreglar; a veces la conclusión correcta es que tu animación no tiene margen para funcionar en ese contexto y debería degradarse. Detectar esto es difícil de forma fiable, pero navigator.hardwareConcurrency y navigator.deviceMemory dan una señal aproximada suficiente para decidir si activas la versión completa o una simplificada.

La lista de comprobación

Antes de dar una animación por terminada, en este orden:

¿Solo animas transform, opacity o filter? Si hay width, height, top, left, margin o box-shadow en la lista de propiedades animadas, tienes layout o pintado por fotograma. Justifícalo o cámbialo.

¿Sobrevive a un bloqueo de 1,2 segundos? Si no, no está en el compositor y vas a depender de que nada más ocurra durante la animación.

¿Cuántas capas crea? Míralo en el panel de capas antes y durante. Si el número crece con la interacción y no baja al terminar, tienes una fuga.

¿Cuánta memoria de vídeo ocupa? Multiplica el área animada por la densidad al cuadrado por cuatro. Si sale por encima de unas decenas de megabytes en móvil, revísalo.

¿Cómo va con estrangulamiento a 4x? Es el mínimo. Con 6x, si tu público incluye gama baja.

¿Qué pasa si el usuario interrumpe a mitad? Pulsa dos veces rápido, cambia de estado a mitad de camino, redimensiona mientras corre. La mayoría de los defectos de animación en producción son de interrupción, no de rendimiento.

¿Qué pasa con el movimiento reducido activado? El nivel anterior entero.

¿Sigue corriendo cuando no se ve? Fuera del viewport, en una pestaña oculta, detrás de un modal. Todo eso es coste sin beneficio.

Casi todo el jank se explica con tres preguntas, y ninguna de las tres es sobre la animación

Después de diagnosticar unos cuantos de estos, aparece un patrón que ahorra muchísimo tiempo: la causa casi nunca está dentro de la animación que se ve mal. Las tres preguntas que resuelven la gran mayoría de los casos son quién más está usando el hilo principal durante esos trescientos milisegundos, cuántas texturas hay vivas en la memoria de la GPU en ese momento, y qué invalidó el layout justo antes. Ninguna de las tres pregunta por la animación; las tres preguntan por su entorno. Y esto no es casualidad, es una consecuencia directa de cómo está construido el navegador: las animaciones bien escritas —transform y opacity sobre una capa estable— cuestan tan poco que es literalmente difícil que sean el cuello de botella, así que cuando una de ellas va mal, la explicación tiene que estar fuera. La implicación práctica es un cambio de hábito que cuesta adquirir porque va contra el instinto: cuando algo se mueve mal, la primera acción no debería ser abrir el código de esa animación, sino grabar tres segundos y mirar qué más hay ahí dentro. El instinto contrario —afinar la curva, bajar la duración, quitar una propiedad de la lista— produce mejoras marginales que a veces enmascaran el problema lo suficiente para que nadie lo vuelva a mirar, y entonces el mismo problema reaparece en la siguiente pantalla con otra animación distinta, y se vuelve a atribuir a esa. Los equipos que se acostumbran a mirar el entorno primero descubren que arreglar una sola causa —un manejador de scroll que fuerza layout, un will-change global, una etiqueta de terceros con un temporizador— mejora simultáneamente veinte animaciones que nadie había relacionado entre sí. Y esa es la señal de que has encontrado la causa de verdad y no un síntoma: cuando el arreglo mejora cosas que no estabas mirando.