Depuración de animaciones CSS con saltos usando DevTools
Las animaciones CSS con saltos (jank) se producen cuando se supera el presupuesto de fotogramas: a 60fps, el navegador dispone de aproximadamente 16,7ms para generar cada fotograma, y cualquier fotograma que se prolongue —por recálculo de layout, pintado o un hilo principal ocupado— reduce la tasa de fotogramas y se manifiesta como un tartamudeo visible. La solución raramente es “añadir más will-change”. Se trata de un diagnóstico: identificar qué etapa del pipeline de renderizado se prolongó y en qué hilo. Este artículo propone un flujo de trabajo sistemático con cuatro paneles en la versión actual de Chrome DevTools —Rendering, Performance, Animations y Layers— para rastrear una animación con saltos hasta su causa, con un ejemplo práctico antes/después que puedes reproducir.
El lector objetivo conoce que transform y opacity son propiedades económicas, pero carece de un procedimiento. El caso canónico es la animación que funcionaba bien en un portátil y presentaba saltos en un Android de gama media. Necesitas un método de triaje, no otro consejo sobre propiedades.
Puntos clave
- A 60fps, el navegador dispone de aproximadamente 16,7ms por fotograma para completar el estilo, el layout, el pintado y la composición; superar ese presupuesto una sola vez produce un salto visible (documentación de rendimiento de Chrome DevTools).
- Diagnostica en orden de paneles: Rendering para una revisión visual rápida, Performance para identificar la causa raíz del fotograma lento, Animations para aislar el keyframe problemático, y Layers para auditar el coste en memoria del compositor.
- Las barras moradas de Recalculate Style o Layout directamente bajo una barra amarilla de JavaScript en la pista Main del panel Performance indican un layout síncrono forzado; el triángulo rojo enlaza a la línea exacta de JS.
- El compositor puede animar
transformyopacitysin necesidad de layout ni pintado; animarleft/top/width/heightfuerza un layout en el hilo principal en cada fotograma (CSS Triggers). - Las animaciones controladas por scroll que usan
animation-timelinese ejecutan en el compositor paratransform/opacity, por lo que sus saltos aparecen en la pista Frames, no como una tarea larga en el hilo principal.
¿Qué es el jank en animaciones?
El jank es un fotograma descartado o retrasado que el usuario puede percibir. Para mantener 60fps, el navegador debe completar cada fotograma en aproximadamente 16,7ms (1000ms ÷ 60). Esa ventana abarca el recálculo de estilos, el layout, el pintado y la composición de ese fotograma. Cuando un solo fotograma se prolonga, el navegador no cumple su plazo, la tasa de fotogramas efectiva cae —a 30fps o menos— y el movimiento parece saltar. La guía de rendimiento de renderizado de Google plantea el mismo presupuesto: los cambios visuales fluidos deben encajar dentro de la ventana por fotograma, y las animaciones son el lugar más visible donde se supera ese presupuesto, porque el ojo sigue el movimiento continuo.
La razón por la que un fotograma lento es perceptible mientras que una petición de red lenta no lo es: la animación es una secuencia de fotogramas que el cerebro integra como movimiento. Un solo fotograma tardío rompe esa integración, y una pausa a mitad del movimiento se percibe como un salto. Por eso “es básicamente fluido” no es suficiente: el peor fotograma, no el promedio, determina la calidad percibida.
El pipeline de renderizado, brevemente
Cada actualización visual recorre un pipeline fijo: parse → style → layout → paint → composite. El navegador analiza el HTML y el CSS para construir el DOM y el CSSOM, calcula qué estilos se aplican (style), determina la geometría y la posición de cada caja (layout), rasteriza los píxeles en capas (paint) y, finalmente, combina las capas en la imagen mostrada (composite). El artículo explicativo del pipeline de renderizado en web.dev es la referencia canónica en profundidad; la versión resumida es todo lo que necesitas aquí.
El punto que sustenta el resto de este artículo: cada etapa del pipeline se ejecuta en un hilo específico y aparece en un panel específico de DevTools. El layout y el pintado se ejecutan en el hilo principal, junto con tu JavaScript. La composición se ejecuta por separado. Una animación que solo compone —mover una capa existente, cambiar su opacidad— evita casi por completo el hilo principal. Una animación que desencadena layout arrastra trabajo de vuelta al hilo principal en cada fotograma, donde compite con todo lo demás. Esa distinción es lo que los paneles que se describen a continuación te permiten ver.
¿Qué paneles de DevTools diagnostican el jank en animaciones?
Discover how at OpenReplay.com.
Un diagnóstico sistemático de jank recorre cuatro paneles en secuencia: el panel Rendering para una comprobación visual rápida (¿hay pintado donde no debería haberlo?), el panel Performance para grabar la animación e identificar la causa raíz del fotograma lento, el panel Animations para aislar qué keyframe o propiedad está causando el problema, y el panel Layers para auditar si la promoción a capa del compositor está ayudando o generando presión en memoria. Úsalos en ese orden: Rendering descarta o confirma categorías enteras de problemas en segundos, Performance te proporciona la traza, Animations acota el problema a la propiedad, y Layers verifica el coste de tu solución.
Panel Rendering: la revisión rápida
El panel Rendering es tu primer paso porque responde, de forma visual e inmediata, si tu animación está repintando cuando no debería. Ábrelo desde el menú de comandos (Cmd/Ctrl+Shift+P, escribe “Show Rendering”) o en More Tools → Rendering (referencia de renderizado de Chrome DevTools). Tres opciones son relevantes:
- Frame Rendering Stats muestra una lectura de FPS en tiempo real y una superposición de memoria GPU mientras se ejecuta la animación. Un valor que cae muy por debajo de 60 durante la animación confirma la existencia de jank.
- Paint flashing resalta las regiones que el navegador repinta iluminándolas en verde. Un elemento que solo anima
transformno debería producir ningún destello verde mientras se mueve; un destello verde que sigue la animación indica que se está desencadenando un pintado. - Layer borders contorna las capas del compositor en naranja. Úsalo para confirmar que un elemento que esperas que esté acelerado por hardware ha obtenido su propia capa, y para detectar capas que no pretendías crear.
Pasos:
- Abre el panel Rendering.
- Activa Paint flashing y desencadena la animación.
- Si el elemento animado destella en verde mientras se mueve, la animación está pintando en cada fotograma: se está animando una propiedad de layout o pintado. Esto confirma un problema a nivel de propiedad e indica que debes abrir el panel Performance a continuación.
- Si no hay destellos pero el movimiento sigue presentando saltos, el cuello de botella probablemente es JavaScript en el hilo principal, no el pintado; también es una cuestión para el panel Performance.
Panel Performance: identificar la causa raíz del fotograma lento
El panel Performance es donde grabas la animación y lees exactamente qué etapa del pipeline superó el presupuesto de fotogramas. Muestra el gráfico de FPS, el tiempo por fotograma en la pista Frames, la actividad del hilo Main y, en la versión actual de Chrome, una barra lateral de Insights que señala automáticamente problemas como el reflow forzado (referencia del panel Performance de Chrome DevTools).
Antes de grabar, limita la CPU para aproximarte al dispositivo donde realmente aparece el jank. La documentación de Chrome DevTools describe los ajustes preestablecidos de limitación de CPU en Capture settings; el panel ofrece un ajuste de “ralentización 4x”, y el enfoque recomendado es probar con una ralentización que se aproxime al hardware de menor potencia (referencia de rendimiento, limitación de CPU). La limitación es importante porque la razón más habitual por la que una animación CSS supera el perfilado local pero presenta saltos en producción es el contexto del dispositivo: un Android de gama media ejecutando Chrome con varias pestañas abiertas dispone de una fracción del presupuesto de CPU de un portátil de desarrollo, algo que la limitación aproxima pero no puede replicar completamente sin simular también la presión de memoria y la carga GPU concurrente.
Pasos:
- Abre el panel Performance y marca Screenshots.
- En Capture settings (icono de engranaje), configura la limitación de CPU con un ajuste preestablecido de ralentización.
- Haz clic en Record, ejecuta la animación durante unos segundos y luego haz clic en Stop.
- Lee primero la pista FPS/Frames. Las marcas rojas sobre los fotogramas señalan los que superaron el presupuesto.
- Amplía un fotograma problemático y examina la pista Main.
Esta es la heurística más útil en la depuración de animaciones:
Barras moradas bajo barras amarillas en la pista Main = layout síncrono forzado. El triángulo rojo es tu enlace directo a la solución.
En la pista del hilo Main, las barras moradas de Recalculate Style o Layout que aparecen directamente bajo una barra amarilla de JavaScript indican un layout síncrono forzado: el navegador se vio obligado a resolver la geometría en mitad del script porque JavaScript leyó una propiedad de layout inmediatamente después de escribir en el DOM. Leer offsetWidth, offsetTop o llamar a getBoundingClientRect() tras una escritura de estilo obliga al navegador a vaciar el layout de forma síncrona; la lista canónica de Paul Irish sobre qué fuerza el layout/reflow enumera estos desencadenantes. El triángulo rojo en la barra morada abre una entrada de Summary con una advertencia de “Layout Forced” y un enlace al archivo fuente con la línea exacta de JS. La guía de layout thrashing de web.dev cubre en profundidad el patrón de lectura tras escritura.
Cuando no hay barras moradas bajo las amarillas, JavaScript completó su trabajo y dejó al navegador realizar el renderizado según su propio calendario. Esa es la traza a la que debes aspirar.
Panel Animations: aislar el keyframe
El panel Animations te permite inspeccionar, desplazarte y ralentizar animaciones activas para localizar el jank en un keyframe o propiedad específicos, en lugar de en la animación en su conjunto. Ábrelo desde More Tools → Animations (documentación del panel Animations de Chrome DevTools). Chrome detecta las animaciones y las lista a medida que se activan, permitiéndote inspeccionar la animación capturada, desplazarte por su línea de tiempo y examinar sus keyframes.
Su potencia diagnóstica surge al combinarlo con Paint flashing. Ralentizar una animación al 10% de velocidad de reproducción mientras se observa Paint flashing en el panel Rendering es la forma más rápida de identificar qué keyframe específico desencadena un repintado: el destello verde aparece en el momento exacto en que el valor de la propiedad problemática entra en efecto.
Pasos:
- Abre el panel Animations y desencadena la animación para que aparezca en la lista.
- Configura la velocidad de reproducción al 10% (los controles están en la parte superior del panel).
- Con Paint flashing activado, desplázate por la línea de tiempo y observa si aparece el destello verde.
- Si el destello verde aparece en un punto específico de la línea de tiempo, centra tu investigación en el keyframe activo en ese momento.
Firefox y otros navegadores incluyen sus propios inspectores de animaciones; aquí se asume Chrome como entorno de trabajo.
Panel Layers: auditar el coste del compositor
El panel Layers muestra qué elementos fueron promovidos a su propia capa del compositor, por qué y a qué coste en memoria, lo que te permite dejar de aplicar will-change indiscriminadamente. Ábrelo desde More Tools → Layers (documentación del panel Layers de Chrome DevTools). Al seleccionar una capa, el panel de detalles muestra su consumo de memoria y el motivo de la composición.
La promoción implica una compensación. Mover un elemento a su propia capa permite al compositor animarlo sin repintar los elementos vecinos, pero cada capa reserva memoria GPU para su textura. La documentación de will-change en MDN es explícita al respecto: la propiedad es un último recurso, ya que aplicarla a demasiados elementos desperdicia recursos porque el navegador ya optimiza por sí solo las propiedades económicas, y la promoción excesiva puede degradar el rendimiento. Usa el panel Layers para contar las capas promovidas y verificar que cada una justifica su coste en memoria.
Ejemplo práctico antes/después: animar left vs. transform
Animar left desencadena un layout en cada fotograma; animar transform: translateX() no desencadena ni layout ni pintado. El mismo movimiento se ejecuta en un hilo diferente. Esta es la versión problemática, que anima left:
/* Problemático: anima una propiedad de layout */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
left: 200px;
}
}
Lo que cada panel muestra con esta versión: el panel Rendering ilumina la caja en verde durante toda la animación, porque cambiar left fuerza un layout, y el layout siempre va seguido de un pintado. La pista Main del panel Performance se llena de barras moradas de Recalculate Style y Layout en cada fotograma, y la pista Frames muestra fotogramas que superan el presupuesto una vez activada la limitación de CPU. Las propiedades left, top, width y height desencadenan layout —consulta CSS Triggers para el desglose por propiedad— y el layout se ejecuta en el hilo principal, donde compite con todo lo demás por el presupuesto de 16,7ms.
La reescritura expresa el mismo movimiento usando únicamente transform:
/* Corregido: anima una propiedad exclusiva del compositor */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
transform: translateX(200px);
}
}
translateX reproduce el cambio de posición mediante transform. Tras la reescritura: Paint flashing no muestra verde durante el movimiento, la pista Main del panel Performance ya no se llena de barras moradas en cada fotograma, y la animación se ejecuta en el compositor. El compositor puede animar transform y opacity sin desencadenar layout ni pintado, por lo que el navegador mueve una textura de capa existente en lugar de recalcular la geometría en cada fotograma.
La lista de correcciones
La solución es un cambio de propiedad: reemplaza todo lo que desencadene layout o pintado con transform u opacity. La tabla relaciona cada intención de animación con su equivalente exclusivo del compositor.
| Intención | Evitar (fuerza layout/pintado) | Usar (exclusivo del compositor) |
|---|---|---|
| Mover | left, top, margin | transform: translate() |
| Redimensionar | width, height | transform: scale() |
| Rotar | hacks que afectan al layout | transform: rotate() |
| Desvanecer | cambios de visibility, cambios de fondo | opacity |
Las propiedades CSS más seguras y con mayor soporte para animar son transform (traslación, escala, rotación, inclinación) y opacity, porque los navegadores generalmente pueden ejecutarlas en el compositor sin desencadenar layout ni pintado. filter también puede estar acelerada por GPU para funciones como blur(), pero el soporte y el comportamiento varían, así que verifícalo en el panel Rendering con Paint flashing antes de asumir que es gratuita: la documentación de filter en MDN describe la propiedad, y CSS Triggers registra su impacto en el renderizado por motor. Muchas otras propiedades animadas desencadenan pintado, y las propiedades que cambian el tamaño o la posición generalmente desencadenan un recálculo de layout en el hilo principal.
Para animaciones controladas por JavaScript, agrupa todas las lecturas del DOM antes de todas las escrituras. El layout síncrono forzado en el ejemplo de la traza proviene de leer una propiedad de layout después de una escritura; agrupar primero las lecturas permite al navegador servirlas desde el layout del fotograma anterior en lugar de vaciar uno nuevo. La guía sobre layout thrashing detalla el patrón.
Usa will-change de forma estratégica, no por defecto. Aplícalo a un elemento que estés a punto de animar y elimínalo cuando la animación termine; según MDN, aplicarlo de forma generalizada desperdicia memoria GPU porque el navegador ya optimiza las propiedades económicas por sí solo. Confirma el efecto en el panel Layers.
Animaciones controladas por scroll: una firma de jank diferente
Las animaciones controladas por scroll declaradas con animation-timeline: scroll() o animation-timeline: view() cambian dónde debes mirar en DevTools. Cuando animan únicamente transform u opacity, se ejecutan en el compositor, por lo que su jank no aparece como una tarea larga en la pista del hilo Main; busca en cambio fotogramas descartados en la pista Frames. La documentación de animation-timeline en MDN y la guía de animaciones controladas por scroll de Chrome cubren la funcionalidad y su compatibilidad con navegadores. Si aplicas la heurística de la pista Main y no encuentras nada, pero la pista Frames sigue mostrando fotogramas que superan el presupuesto, es probable que una propiedad no compositable se haya colado en los keyframes de la animación controlada por scroll.
¿Por qué una animación presenta saltos solo en producción?
DevTools perfila en condiciones controladas; la variable que no puede reproducir completamente es el contexto real del usuario: nivel de CPU del dispositivo, presión de memoria, actividad concurrente. Cuando un informe de jank no se puede reproducir localmente, ese contexto ausente suele ser la causa. La reproducción de sesiones lo captura, de modo que sabes qué condiciones simular antes de grabar.
Ejecuta los cuatro paneles en orden —Rendering para confirmar, Performance para identificar la causa raíz, Animations para aislar, Layers para auditar— y la próxima animación con saltos dejará de ser una suposición.
Preguntas frecuentes
Es el contexto del dispositivo, no el código. Un Android de gama media con varias pestañas abiertas dispone de una fracción del presupuesto de CPU de un portátil. Activa la limitación de CPU en los ajustes de captura del panel Performance, y usa la reproducción de sesiones para capturar las condiciones reales en las que se produjo el jank en producción.
`transform` se ejecuta en el hilo del compositor sin desencadenar layout ni pintado: el navegador simplemente mueve una textura de capa existente en cada fotograma. `left` o `top` fuerzan un recálculo de layout en el hilo principal en cada fotograma, seguido de un pintado, compitiendo con JavaScript por el presupuesto de 16,7ms.
No de forma fiable. Solo `transform` y `opacity` están garantizadas como exclusivas del compositor. `filter` puede estar acelerada por GPU para funciones como `blur()` en algunos motores, pero el soporte varía. Verifícalo en el panel Rendering con Paint flashing: un destello verde indica que está pintando en cada fotograma.
`animation-timeline: scroll()` y `view()` se ejecutan en el compositor cuando animan únicamente `transform` u `opacity`, sin generar ninguna tarea larga en el hilo Main. El jank aparece en la pista Frames. Si la pista Main no muestra nada pero la pista Frames muestra fotogramas que superan el presupuesto, es probable que una propiedad no compositable se haya colado en los keyframes.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.