javascript - eventos de desplazamiento: requestAnimationFrame VS requestIdleCallback VS oyentes de eventos pasivos
dom passive-event-listeners (1)
Como sabemos, a menudo se aconseja rebotar los oyentes de desplazamiento para que el UX sea mejor cuando el usuario está desplazándose.
Sin embargo, a menudo he encontrado bibliotecas y artículos donde personas influyentes como Paul Lewis recomiendan usar requestAnimationFrame
. Sin embargo, a medida que la plataforma web progresa rápidamente, es posible que algunos consejos se deprecien con el tiempo.
El problema que veo es que hay usos muy diferentes para manejar eventos de desplazamiento, como crear un sitio web de paralaje o manejar el desplazamiento infinito y la paginación.
Veo 3 herramientas principales que pueden marcar la diferencia en términos de UX:
Entonces, me gustaría saber, por uso (solo tengo 2, pero puedes encontrar otros), ¿qué tipo de herramienta debo usar ahora para tener una experiencia de desplazamiento muy buena?
Para ser más precisos, mi pregunta principal estaría más relacionada con infinitas vistas de desplazamiento y paginación (que generalmente no tienen que desencadenar animaciones visuales, pero queremos una buena experiencia de desplazamiento), ¿es mejor reemplazar requestAnimationFrame
con un combo de requestIdleCallback
+ pasivo controlador de eventos de desplazamiento? También me pregunto si tiene sentido usar requestIdleCallback
para llamar a una API o manejar la respuesta de la API para que el desplazamiento tenga un mejor rendimiento o es algo que el navegador ya puede manejar para nosotros.
Aunque esta pregunta es un poco más antigua, quiero responderla porque a menudo veo guiones, donde muchas de estas técnicas son mal usadas.
En general, todas sus herramientas solicitadas ( rAF
, rIC
y oyentes pasivos) son excelentes herramientas y no desaparecerán pronto. Pero debes saber por qué usarlos.
Antes de comenzar: en caso de que genere efectos de desplazamiento de desplazamiento / desplazamiento como efectos de paralaje / elementos adhesivos, aceleración con rIC
, setTimeout
no tiene sentido porque quiere reaccionar inmediatamente.
requestAnimationFrame
rAF
le da el punto dentro del ciclo de vida del marco justo antes de que el navegador quiera calcular el nuevo estilo y diseño del documento. Es por eso que es perfecto para usar en animaciones. En primer lugar, no se llamará con más frecuencia o con menos frecuencia que el navegador calcula el diseño (frecuencia correcta). En segundo lugar, se llama justo antes de que el navegador calcule el diseño (momento adecuado). De hecho, utilizar rAF
para cualquier cambio de diseño (cambios DOM o CSSOM) tiene mucho sentido. rAF
se sincroniza con V-SYNC como cualquier otro diseño que represente elementos relacionados en el navegador.
utilizando rAF
para acelerador / antirrebote
El ejemplo predeterminado de Paul Lewis se ve así:
var scheduledAnimationFrame;
function readAndUpdatePage(){
console.log(''read and update'');
scheduledAnimationFrame = false;
}
function onScroll (evt) {
// Store the scroll value for laterz.
lastScrollY = window.scrollY;
// Prevent multiple rAF callbacks.
if (scheduledAnimationFrame){
return;
}
scheduledAnimationFrame = true;
requestAnimationFrame(readAndUpdatePage);
}
window.addEventListener(''scroll'', onScroll);
Este patrón se usa / copia muy a menudo, aunque tiene poco sentido hasta en la práctica. (Y me pregunto por qué ningún desarrollador ve este problema obvio). En general, teóricamente tiene sentido estrangular todo al menos al rAF
, porque no tiene sentido solicitar cambios de diseño del navegador más a menudo que el navegador representa el diseño.
Sin embargo, el evento de scroll
se activa cada vez que el navegador hace que cambie la posición de desplazamiento. Esto significa que un evento de scroll
se sincroniza con la representación de la página. Literalmente es lo mismo que rAF
está dando rAF
. Esto significa que no tiene ningún sentido estrangular algo por algo, que ya está regulado exactamente por definición.
En la práctica, puede verificar lo que acabo de decir agregando un console.log
y verificar con qué frecuencia este patrón "previene múltiples devoluciones de llamada de RFA" (la respuesta es ninguna, de lo contrario sería un error del navegador).
// Prevent multiple rAF callbacks.
if (scheduledAnimationFrame){
console.log(''prevented rAF callback'');
return;
}
Como verá, este código nunca se ejecuta, simplemente es un código muerto.
Pero hay un patrón muy similar que tiene sentido por una razón diferente. Se parece a esto:
//declare box, element, pos
function writeLayout(){
element.classList.add(''is-foo'');
}
window.addEventListener(''scroll'', ()=> {
box = element.getBoundingClientRect();
if(box.top > pos){
requestAnimationFrame(writeLayout);
}
});
Con este patrón, puedes reducir o incluso eliminar la expansión del diseño. La idea es simple: dentro de su oyente de desplazamiento lee el diseño y decide si necesita modificar el DOM y luego llama a la función que modifica el DOM utilizando rAF. ¿Por qué es esto útil? El rAF
se asegura de mover la invalidación del diseño (en el extremo del marco). Esto significa que cualquier otro código que se llame dentro del mismo marco funciona en un diseño válido y puede operar con métodos de lectura de diseño súper rápido.
Este patrón es de hecho tan bueno, que sugeriría el siguiente método auxiliar (escrito en ES5):
/**
* @param fn {Function}
* @param [throttle] {Boolean|undefined}
* @return {Function}
*
* @example
* //generate rAFed function
* jQuery.fn.addClassRaf = bindRaf(jQuery.fn.addClass);
*
* //use rAFed function
* $(''div'').addClassRaf(''is-stuck'');
*/
function bindRaf(fn, throttle){
var isRunning, that, args;
var run = function(){
isRunning = false;
fn.apply(that, args);
};
return function(){
that = this;
args = arguments;
if(isRunning && throttle){return;}
isRunning = true;
requestAnimationFrame(run);
};
}
requestIdleCallback
Es de API similar a rAF
pero da algo totalmente diferente. Le da algunos períodos de inactividad dentro de un marco. (Normalmente, el punto después de que el navegador haya calculado el diseño y la pintura hecha, pero todavía queda algo de tiempo hasta que se produzca la sincronización v). Incluso si la página está retrasada desde la vista de los usuarios, puede haber algunos marcos, donde el navegador es de marcha en vacío. Aunque rIC
puede darte max. 50 ms. La mayoría de las veces solo tienes entre 0.5 y 10ms para cumplir tu tarea. Debido al hecho de que en ese punto del ciclo de vida del marco se rIC
devoluciones de llamada rIC
, no debe alterar el DOM (use rAF
para esto).
Al final tiene mucho sentido estrangular el oyente de scroll
para lazyloading, desplazamiento infinito y el uso de rIC
. Para este tipo de interfaces de usuario, puede incluso acelerar más y agregar un setTimeout
delante de él. (Entonces espera rIC
y luego un rIC
) (Ejemplo de vida para un rebote y un acelerador ).
Aquí también hay un artículo sobre rAF
, que incluye dos diagramas que pueden ayudar a comprender los diferentes puntos dentro de un "ciclo de vida del marco".
Oyente de eventos pasivos
Los oyentes de eventos pasivos se inventaron para mejorar el rendimiento de desplazamiento. Los navegadores modernos movieron el desplazamiento de página (representación en espiral) desde el hilo principal al hilo de composición. (ver https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/ )
Pero hay eventos que producen desplazamiento, que pueden evitarse mediante secuencias de comandos (lo que sucede en el hilo principal y, por lo tanto, pueden revertir la mejora del rendimiento).
Lo que significa que tan pronto como uno de estos eventos oyentes esté vinculado, el navegador debe esperar a que se ejecute este oyente antes de que el navegador pueda calcular el desplazamiento. Estos eventos son principalmente touchstart
, touchmove
, touchend
, wheel
y, en teoría, hasta cierto punto, keypress
y keydown
. El evento de scroll
sí no es uno de estos eventos. El evento de scroll
no tiene una acción predeterminada, que puede evitarse mediante una secuencia de comandos.
Esto significa que si no usa preventDefault
en su touchstart
, touchmove
, touchend
y / o wheel
, siempre use oyentes de eventos pasivos y debería estar bien.
En caso de que utilice prevenirDefault, compruebe si puede sustituirlo con la propiedad CSS touch-action
o bájelo al menos en su árbol DOM (por ejemplo, no hay delegación de eventos para estos eventos). En el caso de escuchas de wheel
es posible que pueda vincularlos / desvincularlos en mouseenter
/ mouseleave
.
En caso de cualquier otro evento: no tiene sentido utilizar oyentes de eventos pasivos para mejorar el rendimiento. Lo más importante a tener en cuenta: el evento de scroll
no se puede cancelar y, por lo tanto, nunca tiene sentido utilizar oyentes de eventos pasivos para scroll
.
En el caso de una vista de desplazamiento infinito, no necesita el touchmove
, solo necesita scroll
, por lo que los escuchas de eventos pasivos ni siquiera se aplican.
Currículum
Para responder tu pregunta
- para lazyloading, infinite view usa una combinación de
setTimeout
+requestIdleCallback
para tus oyentes de eventos y usarAF
para cualquier escritura de diseño (mutaciones DOM). - para los efectos instantáneos todavía usan
rAF
para cualquier escritura de diseño (mutaciones DOM).