La inversión: separar la animación del tiempo
De calcular el progreso y escribir estilos cada frame a declarar el efecto una vez y conectarlo a una fuente. El cambio de modelo, lo que se gana y lo que se pierde.
El código que anima según el desplazamiento tiene siempre la misma forma: escuchar, medir, calcular una fracción, escribir estilos. Está repartido entre un manejador de eventos y unas cuantas constantes, no es un objeto, no se puede pausar ni inspeccionar, y corre entero en el hilo principal. La abstracción de las líneas de tiempo lo sustituye por otra cosa: declaras el efecto una vez y le enchufas una fuente de progreso. Es la misma inversión de control que llevó de manipular el DOM a describirlo, y tiene las mismas ventajas y el mismo precio.
- Contrastar la forma imperativa y la declarativa del mismo efecto.
- Enumerar qué se gana al convertir la animación en un objeto de la plataforma.
- Identificar qué efectos no se pueden expresar en el modelo declarativo.
- Situar esta inversión junto a otras equivalentes de la plataforma.
Las dos formas
flowchart TB S1[el usuario desplaza] --> S2[evento de scroll en el hilo principal] S2 --> S3[JS mide y calcula el progreso] S3 --> S4[JS escribe estilos] S4 --> S5[frame con al menos un frame de retraso] D1[el usuario desplaza] --> D2[fuente de progreso] D2 --> D3[efecto declarado una sola vez] D3 --> D4[frame sin codigo propio] style S3 fill:#f38ba8,color:#11111b style S4 fill:#f38ba8,color:#11111b style S5 fill:#f9e2af,color:#11111b style D2 fill:#cba6f7,color:#11111b style D3 fill:#89b4fa,color:#11111b style D4 fill:#a6e3a1,color:#11111b
La versión imperativa, escrita con todo el cuidado que se le puede poner:
const barra = document.querySelector('.progreso');
let alto = 0;
function medir() {
alto = document.documentElement.scrollHeight - innerHeight;
}
medir();
addEventListener('resize', medir);
let pendiente = false;
addEventListener('scroll', () => {
if (pendiente) return;
pendiente = true;
requestAnimationFrame(() => {
const p = alto > 0 ? Math.min(1, scrollY / alto) : 0;
barra.style.transform = `scaleX(${p})`;
pendiente = false;
});
}, { passive: true });
Veinte líneas para una barra de progreso, y ya están aplicadas todas las buenas prácticas: la medida cacheada y recalculada solo al redimensionar, el trabajo agrupado en un requestAnimationFrame, la protección contra ejecutar más de una vez por frame, y la guarda contra la división por cero. Quitar cualquiera de esas cuatro cosas es un bug de rendimiento.
Y a pesar de todo eso, el resultado sigue teniendo tres problemas estructurales que no se pueden arreglar desde dentro: el trabajo ocurre en el hilo principal, el evento llega describiendo un desplazamiento que el compositor ya ha pintado, y la animación no es un objeto que nadie pueda inspeccionar.
La forma declarativa dice lo mismo de otra manera: existe un efecto —una escala horizontal que va de 0 a 1— y existe una fuente de progreso —el recorrido de desplazamiento del documento—, y el efecto se conecta a la fuente. No hay evento, no hay medida, no hay cálculo de fracción y no hay escritura de estilos, porque las cuatro cosas son responsabilidad del motor, que ya las hacía por su cuenta para otros fines.
Qué se gana
El efecto vuelve a ser un objeto. Aparece en getAnimations(), tiene playState, se puede pausar, inspeccionar en el panel de animaciones, cancelar y consultar. Un manejador de scroll con un style.transform dentro no es nada de eso: es código.
Desaparece el retraso estructural. El desplazamiento se procesa donde se conoce, sin dar la vuelta por el hilo principal. La animación no puede ir un frame por detrás del contenido porque no hay dos sitios donde pueda desfasarse.
Desaparece el trabajo por frame. No es que se optimice: es que no existe. Cero manejadores, cero escrituras al DOM, cero recálculos de estilo provocados por ti.
Se puede escribir en CSS. Y por tanto participa de las consultas de medios, de las capas, de la cascada y de todo lo que el CSS sabe hacer con el contexto. Un efecto que solo debe existir en pantallas grandes es una consulta de medios, no un if dentro de un manejador.
Se reutiliza todo lo aprendido. Keyframes, easing por tramo, fill, composite, direction. El efecto no cambia porque cambie su fuente.
Qué se pierde
La libertad. El modelo declarativo solo sabe expresar una cosa: interpolar entre keyframes según un progreso. Si tu efecto es eso, encaja perfectamente. Si no lo es, no encaja de ninguna manera.
No encaja nada que dependa del estado anterior: un suavizado con inercia, un valor que persigue a otro, cualquier cosa con memoria. El progreso es una función pura de la posición actual y no lleva historia.
No encaja nada que dependa de la velocidad o la dirección del movimiento: un elemento que se inclina según lo rápido que vayas, un indicador que cambia según subas o bajes. La fuente da posición, no derivada.
No encaja el dibujo directo. Un canvas no tiene propiedades animables, así que necesita su bucle, aunque puede leer el progreso de una animación declarativa para ir en fase con ella, que es el patrón híbrido que ya conoces.
Y no encaja nada que necesite ejecutar lógica en función del progreso: cargar datos al llegar al 40%, disparar un evento analítico, cambiar el contenido de un nodo. Para eso siguen estando los observadores y los manejadores; simplemente ya no tienen que encargarse además del movimiento.
Esta inversión no es la primera de la plataforma y merece la pena verla en su serie, porque el patrón se repite y predice bastante bien qué llegará después. setInterval a requestAnimationFrame: dejas de decidir cuándo y pasas a participar en un ritmo que impone el sistema. Manejadores de scroll a IntersectionObserver: dejas de preguntar continuamente y declaras qué te interesa. element.style en un bucle a Element.animate(): dejas de escribir valores y declaras una interpolación. Y ahora, fuente de tiempo a fuente de progreso: dejas de calcular el avance y declaras de dónde viene. En los cuatro casos el movimiento es el mismo —renunciar a control imperativo a cambio de que el motor pueda hacer el trabajo en el sitio y el momento correctos— y en los cuatro casos la ganancia real no fue el rendimiento sino que el navegador pasó a saber qué estabas intentando hacer. Un manejador de scroll es opaco: el motor no puede optimizarlo, ni pausarlo, ni ejecutarlo en otro hilo, ni enseñártelo en un panel, porque solo ve código. Una animación declarada es transparente, y todo eso viene gratis. La consecuencia práctica para tu trabajo diario: cuando dudes entre expresar algo con una API declarativa que se queda un poco corta y con código imperativo que hace exactamente lo que quieres, cuenta también lo que pierdes en herramientas, no solo en milisegundos.
Cómo se decide
La pregunta que discrimina es una sola: ¿tu efecto es una función pura de la posición actual del recorrido? Si al saltar directamente al 60% del recorrido el resultado visual debe ser el mismo que si hubieras llegado ahí desplazándote poco a poco, entonces sí, y el modelo declarativo lo cubre entero.
Si el resultado debe depender de cómo llegaste —con qué velocidad, desde arriba o desde abajo, cuánto tiempo estuviste parado— entonces no es una función de la posición y necesitas código. Es un criterio operativo y contestable en diez segundos para cualquier efecto concreto.
En la práctica, la enorme mayoría de los efectos de desplazamiento que se ven en la web son funciones puras de la posición: aparecer al entrar, moverse a otro ritmo que el contenido, teñirse, encogerse, avanzar una barra. Ese es el porcentaje del código de scroll que esta abstracción hace desaparecer.
Coge tres efectos de desplazamiento de páginas que conozcas y clasifícalos aplicando el criterio: si saltaras al 60% del recorrido de golpe, ¿el resultado sería el mismo? Anota cuáles pasan la prueba y cuáles no, y para los que no, identifica exactamente de qué dependen además de la posición. Es la lista de lo que seguirá necesitando JavaScript.