wandres.dev
LOTTIE Y AFTER EFFECTS · La animación del diseñador

El coste real en runtime: medirlo, no estimarlo

Los cuatro costes de una animación Lottie y dónde se paga cada uno, cómo medir el trabajo por fotograma con datos y no con impresiones, y qué presupuesto es defendible.

⏱ 21 min

Lottie tiene fama de caro y la fama es merecida a medias, que es la peor situación posible: la mitad de los equipos lo prohíben por rumores y la otra mitad mete cuatro bucles infinitos en la portada. Lo que hay debajo es un intérprete de un formato de animación completo, ejecutándose en el hilo principal, en cada fotograma, durante todo el tiempo que la animación esté activa. Ese coste es real, es medible con precisión y admite un presupuesto. Vamos a medirlo.

🎯 Al terminar esta lección sabrás
  • Separar los cuatro costes de una animación Lottie e identificar en qué momento se paga cada uno.
  • Instrumentar una página para obtener el tiempo de trabajo por fotograma del reproductor.
  • Usar la API de fotogramas largos de animación para atribuir el trabajo al script correcto.
  • Fijar un presupuesto defendible y saber qué palanca tocar cuando no se cumple.

Los cuatro costes

Se confunden constantemente y cada uno se paga en un momento distinto y se arregla con una palanca distinta.

Transferencia. Los bytes del .json o del .lottie. Se paga una vez, en la red, y se arregla comprimiendo, cargando bajo demanda o simplificando la animación. Es el coste que todo el mundo mide y el que menos suele importar.

Parseo. JSON.parse de un fichero de Lottie es una operación síncrona en el hilo principal. Un JSON de cuatro megabytes lleno de coordenadas puede tardar decenas de milisegundos en un móvil de gama media, y ese tiempo es un bloqueo duro: nada más ocurre mientras tanto. Es el coste responsable de que una página con Lottie “se quede pillada un momento” al cargar, y no aparece en ninguna métrica de tamaño.

Inicialización. Construir el árbol interno del reproductor, crear los nodos del SVG o instanciar el módulo WebAssembly, resolver las precomposiciones. También síncrono, también una vez.

Interpretación por fotograma. El coste continuo. Cada fotograma el reproductor recorre las capas, evalúa las propiedades animadas, resuelve la jerarquía de transformaciones, aplica los modificadores y emite el resultado. Este es el que determina si tu página va a sesenta fotogramas o no, y el único que se acumula con el tiempo.

Los tres primeros se arreglan con carga diferida; el cuarto no se arregla con nada que no sea cambiar la animación.

Medir el trabajo por fotograma

La medida que quieres no es “cuántos fotogramas por segundo va la animación” —eso lo limita el navegador y siempre parecerá bien mientras haya margen—, sino cuántos milisegundos de hilo principal consume el reproductor en cada fotograma. Con eso puedes decidir si cabe en el presupuesto junto al resto de tu página.

La forma más limpia es marcar el trabajo del reproductor con la API de User Timing, que además pinta las marcas en la pista de Timings del panel de rendimiento.

import lottie from "lottie-web";

const animacion = lottie.loadAnimation({
  container: document.querySelector("#caja"),
  renderer: "svg",
  loop: true,
  autoplay: true,
  path: "/animaciones/hero.json",
});

// enterFrame se dispara justo antes de que el reproductor dibuje el fotograma.
animacion.addEventListener("enterFrame", () => performance.mark("lottie-inicio"));

// El trabajo del reproductor termina antes de que el navegador componga,
// asi que cerramos la medida al principio del siguiente rAF.
function cerrar() {
  performance.mark("lottie-fin");
  try {
    performance.measure("lottie", "lottie-inicio", "lottie-fin");
  } catch {}
  requestAnimationFrame(cerrar);
}
requestAnimationFrame(cerrar);

// Resumen cada 5 segundos.
setInterval(() => {
  const medidas = performance.getEntriesByName("lottie", "measure");
  if (!medidas.length) return;
  const tiempos = medidas.map((m) => m.duration).sort((a, b) => a - b);
  const p = (q) => tiempos[Math.floor(tiempos.length * q)].toFixed(2);
  console.log(`lottie ms/frame  mediana ${p(0.5)}  p95 ${p(0.95)}  max ${tiempos.at(-1).toFixed(2)}`);
  performance.clearMeasures("lottie");
}, 5000);

La mediana te dice el coste habitual y el p95 te dice si hay fotogramas puntuales que se disparan, que es lo que produce el tirón perceptible. Un p95 tres veces mayor que la mediana casi siempre significa que hay una parte de la animación —un momento con muchas formas visibles a la vez, o una máscara que aparece— mucho más cara que el resto.

La segunda herramienta es la API de fotogramas largos de animación, que reporta cualquier fotograma que haya tardado más de cincuenta milisegundos e incluye la atribución al script culpable. Está disponible en Chromium; en otros motores el try evita el error.

try {
  new PerformanceObserver((lista) => {
    for (const entrada of lista.getEntries()) {
      console.warn(
        `Frame largo de ${entrada.duration.toFixed(0)} ms`,
        `render ${entrada.renderStart ? (entrada.styleAndLayoutStart - entrada.renderStart).toFixed(0) : "?"} ms`,
        entrada.scripts.map((s) => `${s.sourceURL} ${s.duration.toFixed(0)} ms`)
      );
    }
  }).observe({ type: "long-animation-frame", buffered: true });
} catch {}

Si el sourceURL que aparece es el del reproductor, ya no hay discusión sobre de quién es el coste.

Y la tercera, que es la que usarás para diagnosticar: el panel de rendimiento con estrangulamiento de CPU a 4x o 6x. Sin estrangular, tu portátil disimula cualquier problema. Con 4x —que aproxima un Android de gama media— un reproductor de SVG con cuarenta formas pasa de invisible a evidente. Graba cinco segundos, busca la pista del hilo principal y mira qué proporción de cada fotograma ocupa el bloque del reproductor.

ℹ️
Compara siempre contra la línea base

Mide la página sin la animación y luego con ella. El número absoluto no significa nada; la diferencia sí. Y mide con el resto de la página funcionando: una animación que consume tres milisegundos en una página vacía puede ser la gota que colma el vaso en una que ya usa doce.

Qué domina el coste

Con instrumentación puesta, estos son los factores que mueven la aguja, en orden de impacto observado:

Número de formas dibujadas simultáneamente. El factor dominante, por mucho. No el número de capas del fichero: las que están visibles en el fotograma actual, contando las copias generadas por repetidores. Una animación que pasa de veinte a doscientas formas visibles multiplica el coste por diez, no por dos.

Máscaras y mattes. En el renderizador de SVG, cada matte implica componer capas con recorte. Es la operación más cara del formato y la que peor escala.

Área en píxeles. En canvas, dominante. Un canvas a pantalla completa en un móvil con densidad 3 son varios millones de píxeles por fotograma. En SVG afecta al coste de repintado, que también crece con el área.

Gradientes animados. Un gf con paradas animadas obliga a reconstruir el degradado cada fotograma. Un gradiente estático se puede cachear; uno animado no.

Los enterFrame con oyentes. Cada oyente registrado se ejecuta sesenta veces por segundo. La instrumentación de arriba tiene coste; quítala en producción.

Lo que no domina, aunque todo el mundo lo cita: el tamaño del fichero y el número total de keyframes. Un fichero con veinte mil keyframes y cinco formas visibles es barato, porque interpolar es una multiplicación y dibujar es rellenar píxeles.

El presupuesto y las palancas

Un presupuesto defendible, con la lógica detrás en lugar de un número mágico:

A sesenta fotogramas por segundo el presupuesto total del fotograma es 16,7 ms, de los cuales el navegador se queda una parte para su propio trabajo. Si asignas a toda la animación decorativa el 20% de lo que queda, salen unos 2 ms de hilo principal por fotograma para el conjunto de las animaciones Lottie de la pantalla. Medido con estrangulamiento de CPU a 4x, porque ese es el dispositivo del usuario, no el tuyo.

Cuando no cabe, hay cinco palancas y conviene tirar de ellas en este orden, porque van de menos a más coste de negociación.

No reproducir lo que no se ve. Gratis, y elimina el coste por completo en la mayoría de las páginas con varias animaciones.

Bajar la densidad de píxel en canvas. Limitar a 2 el dpr en un dispositivo con 3 elimina más de la mitad de los píxeles y en una animación pequeña es indistinguible.

Cambiar de renderizador o de motor. SVG a canvas, o lottie-web a un motor nativo. Es una decisión técnica que no toca la animación.

Recortar formas visibles. Aquí ya hay que hablar con quien la hizo, pero suele haber margen: partículas de fondo que nadie mira, copias de un repetidor que no se distinguen, capas ocultas exportadas por error.

Cambiar de técnica. Si después de todo lo anterior sigue sin caber, el problema no es el reproductor: es que esa animación no debía ser un Lottie. Esa conversación es la de la lección siguiente.

Lo que pagas por Lottie es la generalidad, y la generalidad no se puede optimizar

Hay una razón estructural por la que un Lottie siempre costará más que la misma animación escrita a mano, y no tiene que ver con la calidad del reproductor: el reproductor no sabe qué animación va a reproducir. Cuando tú escribes una animación con CSS o con una librería, tú sabes que solo se mueven tres elementos, que solo cambian transform y opacity, y que no hay máscaras; el navegador puede promocionar esas tres capas al compositor y no volver a tocar el hilo principal hasta que termine. Un intérprete de Lottie no puede asumir nada de eso, porque el mismo código tiene que poder reproducir un icono de tres formas y una ilustración de doscientas con mattes anidados y trazados que cambian de geometría en cada fotograma. Un trazado cuya geometría cambia no se puede promocionar a una capa: hay que redibujarlo. Un matte no se puede delegar: hay que componerlo. Así que el intérprete hace en el hilo principal, cada fotograma, un trabajo que en una animación escrita a mano se habría hecho una vez y delegado al compositor. Esto no es un defecto que una versión futura vaya a arreglar; es el precio exacto de la generalidad del formato, y es el mismo precio que paga cualquier intérprete frente a cualquier código especializado. La consecuencia para tus decisiones es concreta y no depende de qué reproductor esté de moda: usa Lottie cuando de verdad necesites la generalidad —cuando la pieza sea una ilustración con decenas de elementos que ninguna persona sensata escribiría a mano— y no la uses para lo que sí escribirías a mano, por muy cómodo que sea recibir el fichero ya hecho. Un spinner de Lottie es un intérprete completo de un formato de animación, cargado, parseado e instanciado, para dibujar un arco que gira. Ese arco son cuatro líneas de CSS que corren en el compositor y cuestan literalmente cero en el hilo principal.