El presupuesto de un fotograma y qué cabe dentro
Los milisegundos reales que tienes según la frecuencia del panel, por qué el navegador se queda una parte, el precipicio de perder un fotograma, y el reparto concreto que cabe en el presupuesto.
La cifra de dieciséis milisegundos y medio se repite tanto que ha dejado de significar algo. No es el presupuesto que tienes: es el intervalo entre dos refrescos de un panel de sesenta hercios, del cual el navegador necesita una parte para su propio trabajo, y del cual una fracción creciente de tus usuarios ni siquiera dispone porque su pantalla va al doble. Poner números correctos a esto cambia qué decisiones parecen razonables.
- Calcular el presupuesto real de un fotograma según la frecuencia de refresco.
- Explicar por qué pasarse un milisegundo cuesta un fotograma entero.
- Repartir el presupuesto entre las etapas del pipeline con cifras defendibles.
- Medir la frecuencia de refresco y la tasa de fotogramas perdidos del dispositivo real.
Los números de verdad
El intervalo entre refrescos sale de dividir mil entre los hercios del panel:
| Frecuencia | Intervalo | Presupuesto útil aproximado |
|---|---|---|
| 30 Hz | 33,33 ms | 28 ms |
| 60 Hz | 16,67 ms | 10 ms |
| 90 Hz | 11,11 ms | 7 ms |
| 120 Hz | 8,33 ms | 5 ms |
| 144 Hz | 6,94 ms | 4 ms |
La tercera columna es la que importa y la que casi nunca se cita. El intervalo no es tuyo entero. El navegador necesita una parte de cada ciclo para su propio trabajo: recoger la entrada, hacer el commit, coordinar el rasterizado, componer y presentar. La recomendación clásica de las guías de rendimiento —dejar unos diez milisegundos de los dieciséis para tu trabajo— es exactamente eso: un margen de un tercio para la maquinaria.
Y a sesenta hercios, esos diez milisegundos tienen que cubrir todo lo que va del hilo principal: tus manejadores de entrada, tus devoluciones de fotograma, el recálculo de estilo, el layout, la pintura y el commit. No es mucho.
A ciento veinte hercios, que es la frecuencia de una parte creciente de los móviles de gama alta y de los portátiles recientes, el presupuesto útil baja a unos cinco milisegundos. Es la mitad de trabajo por fotograma con la misma cantidad de píxeles que producir.
El precipicio
Aquí está la propiedad que hace que el rendimiento de animación se comporte de forma tan poco intuitiva: el coste de pasarse no es proporcional, es cuantizado.
Si tu fotograma tarda 15 milisegundos a sesenta hercios, llegas. Si tarda 17, no llegas por dos milisegundos y el resultado no es un retraso de dos milisegundos: es que ese fotograma se pierde y el siguiente refresco muestra el anterior. La latencia real pasa de 16,7 a 33,3 milisegundos. Dos milisegundos de exceso han costado dieciséis y medio de latencia percibida.
Esa discontinuidad tiene dos consecuencias prácticas.
La primera: no hay grados de “casi va bien”. Una animación a 60 fotogramas por segundo y una a 58 no se diferencian en un 3%: la segunda tiene dos tirones por segundo, y el ojo humano los detecta con una facilidad notable, porque lo que se percibe no es la media sino la irregularidad.
La segunda: la optimización que baja de 17 a 15 milisegundos vale muchísimo más que la que baja de 30 a 25. La primera cruza el umbral y duplica la fluidez percibida; la segunda mejora un caso que ya estaba perdido. Cuando priorizas trabajo de rendimiento de renderizado, mira dónde están los umbrales, no los porcentajes.
Hay un matiz que suaviza la primera consecuencia en los dispositivos modernos: muchos paneles tienen frecuencia variable, y el sistema puede bajar de 120 a 60 hercios de forma coherente en lugar de perder fotogramas sueltos. Una animación que sostiene 60 constantes en un panel de 120 se ve bien; una que oscila entre 90 y 50 se ve fatal aunque su media sea mejor. La constancia importa más que el número.
Qué cabe dentro
Un reparto defendible para un fotograma de sesenta hercios, con los diez milisegundos útiles:
| Parte | Presupuesto |
|---|---|
| Manejadores de entrada y devoluciones de fotograma | 3-4 ms |
| Recálculo de estilo | 1-2 ms |
| Layout | 2-3 ms |
| Pintura y commit | 1-2 ms |
| Margen | 1-2 ms |
Y lo que eso significa en términos concretos, que es lo útil:
Cabe: actualizar el estilo de unas decenas de elementos, calcular el layout de un subárbol acotado, ejecutar una devolución de fotograma que haga aritmética sobre unos cientos de valores, animar posiciones de una lista corta.
No cabe: volver a renderizar un árbol de componentes de mil nodos, recalcular el estilo del documento entero, hacer layout de una lista de cinco mil filas, parsear una respuesta de red de tamaño medio, ordenar diez mil objetos.
Y hay una tercera categoría que es la más importante: lo que no consume presupuesto en absoluto. Una animación de transform u opacity gestionada por el motor no toca el hilo principal, así que su presupuesto no es de diez milisegundos: es de cero. Esa es la razón de que la estrategia correcta ante un problema de fluidez casi nunca sea optimizar el trabajo del fotograma, sino sacar la animación del hilo principal para que no haya trabajo de fotograma.
Dicho de otra forma: el presupuesto de fotograma es una herramienta para razonar sobre lo que tiene que ocurrir en el hilo principal. Todo lo que se pueda sacar de ahí no compite por él.
Medir el presupuesto real del dispositivo
Dos mediciones, y las dos hay que hacerlas en el dispositivo de referencia y no en el de desarrollo.
La frecuencia de refresco real, que no es una constante y que en muchos dispositivos depende del estado de la batería y del modo de ahorro:
async function frecuenciaDePanel(muestras = 90) {
const marcas = [];
await new Promise((resolver) => {
requestAnimationFrame(function paso(t) {
marcas.push(t);
marcas.length < muestras ? requestAnimationFrame(paso) : resolver();
});
});
const deltas = marcas.slice(1).map((t, i) => t - marcas[i]).sort((a, b) => a - b);
const mediana = deltas[Math.floor(deltas.length / 2)];
return { hz: Math.round(1000 / mediana), intervaloMs: +mediana.toFixed(2) };
}
console.log(await frecuenciaDePanel());
La tasa de fotogramas perdidos durante una interacción, que es la métrica que de verdad describe la experiencia:
function medirFluidez(duracionMs = 3000) {
return new Promise((resolver) => {
const intervalos = [];
let previo = performance.now();
const fin = previo + duracionMs;
requestAnimationFrame(function paso(t) {
intervalos.push(t - previo);
previo = t;
if (t < fin) requestAnimationFrame(paso);
else {
const objetivo = intervalos.slice().sort((a, b) => a - b)[Math.floor(intervalos.length / 2)];
const perdidos = intervalos.filter((d) => d > objetivo * 1.5).length;
resolver({
fotogramas: intervalos.length,
perdidos,
porcentajePerdido: +((perdidos / intervalos.length) * 100).toFixed(1),
peorIntervalo: +Math.max(...intervalos).toFixed(1),
});
}
});
});
}
Ejecuta esa función mientras haces la interacción que te preocupa. El campo porcentajePerdido es la cifra que se puede llevar a una discusión: por debajo del 2% la animación se percibe fluida, por encima del 10% se percibe rota, y en medio depende del tipo de movimiento.
La idea de que hay que alcanzar sesenta fotogramas por segundo cueste lo que cueste es la simplificación más extendida del tema, y lleva a decisiones malas en al menos tres situaciones concretas. La formulación correcta es otra: el objetivo es una cadencia constante que el dispositivo pueda sostener, y ese número no siempre es sesenta.
Situación uno: el dispositivo no puede. Una animación compleja en un teléfono de gama baja puede oscilar entre 45 y 25 fotogramas por segundo. El resultado se percibe como un tartamudeo continuo. Si la misma animación se simplifica hasta que el dispositivo sostiene 30 constantes, se percibe mejor, aunque la media sea peor. Es contraintuitivo y es cierto: el cine lleva un siglo funcionando a veinticuatro fotogramas por segundo, y nadie lo describe como entrecortado, porque la cadencia es perfectamente regular.
La forma de conseguirlo en la práctica es detectar que el dispositivo no llega y degradar de forma consciente en lugar de dejar que se degrade solo:
const { porcentajePerdido } = await medirFluidez(1500);
if (porcentajePerdido > 15) {
document.documentElement.dataset.animacion = 'reducida';
}/* La version completa */
.tarjeta { transition: transform 300ms cubic-bezier(.2,.8,.2,1), box-shadow 300ms; }
/* La version que el dispositivo sostiene */
[data-animacion='reducida'] .tarjeta { transition: transform 150ms linear; box-shadow: none; }Situación dos: el movimiento no lo necesita. Un desvanecimiento de opacidad se percibe idéntico a 30 y a 60 fotogramas por segundo, porque no hay desplazamiento espacial que seguir. El movimiento rápido y de gran recorrido —un arrastre, un desplazamiento, un elemento que cruza la pantalla— sí necesita cadencia alta, porque el ojo persigue la posición y detecta los saltos. Gastar presupuesto en subir la cadencia de un desvanecimiento es tirarlo.
Situación tres: hay algo más importante ocurriendo. Durante una carga o una transición de página, mantener sesenta fotogramas por segundo en una animación decorativa a costa de retrasar el contenido es una mala priorización. Ahí la animación puede caer a treinta sin que nadie lo note, y los milisegundos liberados van al trabajo que el usuario está esperando.
Y ahora la parte que convierte esto en método: la preferencia de movimiento reducido del sistema es la vía de escape más limpia y la más infrautilizada. No es solo accesibilidad, aunque sea principalmente accesibilidad. Es un interruptor que el usuario ya ha configurado y que te autoriza a no hacer el trabajo:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Ese bloque, que mucha gente copia como casilla de accesibilidad, es además la mejor optimización de renderizado que existe para los usuarios que lo tienen activado: elimina el trabajo de fotograma entero en lugar de reducirlo. Y en dispositivos de gama baja con modo de ahorro de energía, la preferencia viene activada de fábrica con más frecuencia de lo que la gente cree, de modo que ese bloque está sirviendo exactamente al segmento que peor lo pasaba.
La conclusión que me llevo de todo el nivel, y que es la que enlaza con lo que viene: el presupuesto de fotograma es una restricción sobre el trabajo del hilo principal, y la mejor forma de cumplir una restricción es no estar sujeto a ella. Todo lo que se pueda resolver en el compositor, todo lo que se pueda hacer una vez en lugar de por fotograma, y todo lo que se pueda no hacer, sale del presupuesto por completo. Optimizar el trabajo que hay dentro del fotograma es la última opción, no la primera.
Mide la frecuencia real de tu dispositivo de referencia y calcula su presupuesto útil. Después ejecuta la medición de fluidez durante tu interacción animada más pesada y anota el porcentaje de fotogramas perdidos. Si pasa del 10%, prueba la degradación consciente: simplifica la animación hasta que el porcentaje baje del 2% y compara las dos versiones en vídeo. Casi siempre gana la simplificada, y casi nunca es lo que se espera.