web-dev-qa-db-fra.com

événements de défilement: requestAnimationFrame VS requestIdleCallback VS écouteurs d'événements passifs

Comme nous le savons, il est souvent conseillé de supprimer les écouteurs de défilement afin que l'UX soit meilleure lorsque l'utilisateur fait défiler.

Cependant, j'ai souvent trouvé bibliothèques et articles où des personnalités telles que Paul Lewis recommandent d'utiliser requestAnimationFrame. Cependant, à mesure que la plate-forme Web progresse rapidement, il est possible que certains conseils deviennent obsolètes au fil du temps.

Le problème que je vois est qu'il existe des cas d'utilisation très différents pour la gestion des événements de défilement, comme la création d'un site Web de parallaxe, ou la gestion d'un défilement et d'une pagination infinis.

Je vois 3 outils majeurs qui peuvent faire la différence en terme d’UX:

Donc, j'aimerais savoir, par cas (je n'en ai que 2, mais vous pouvez en proposer d'autres), quel type d'outil devrais-je utiliser maintenant pour avoir une très bonne expérience de défilement?

Pour être plus précis, ma question principale serait davantage liée aux vues et à la pagination à défilement infini (qui ne doivent généralement pas déclencher d'animations visuelles, mais nous souhaitons une bonne expérience de défilement). Est-il préférable de remplacer requestAnimationFrame par un combo de requestIdleCallback + gestionnaire d'événement de défilement passif? Je me demande également quand il est judicieux d'utiliser requestIdleCallback pour appeler une API ou gérer la réponse de cette dernière afin de permettre au défilement de mieux fonctionner, ou s'agit-il déjà d'une solution que le navigateur gère déjà pour nous?

38
Sebastien Lorber

Bien que cette question soit un peu plus ancienne, je souhaite y répondre car je vois souvent des scripts dans lesquels bon nombre de ces techniques sont mal utilisées.

En général, tous les outils demandés (rAF, rIC et auditeurs passifs) sont d’excellents outils et ne disparaîtront pas de sitôt. Mais vous devez savoir pourquoi les utiliser.

Avant de commencer: si vous générez des effets liés synchronisés/liés de défilement, tels que des effets de parallaxe/éléments collants, limiter l'utilisation de rIC, setTimeout n'a pas de sens car vous souhaitez réagir immédiatement.

requestAnimationFrame

rAF vous donne le point dans le cycle de vie du cadre juste avant que le navigateur veuille calculer le nouveau style et la nouvelle présentation du document. C'est pourquoi il est parfait pour les animations. D'abord, il ne sera pas appelé plus souvent ou moins souvent que le navigateur ne calcule la mise en page (fréquence correcte). Deuxièmement, il est appelé juste avant que le navigateur ne calcule la mise en page (timing correct). En fait, utiliser rAF pour toute modification de mise en page (modifications DOM ou CSSOM) a beaucoup de sens. rAF est synchronisé avec le V-SYNC comme tout autre élément lié au rendu de la présentation dans le navigateur.

en utilisant rAF pour throttle/debounce

L'exemple par défaut de Paul Lewis ressemble à ceci:

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);

Ce modèle est très souvent utilisé/copié, bien qu’il ait peu de sens en pratique. (Et je me demande pourquoi aucun développeur ne voit ce problème évident.) En général, théoriquement, il est logique de tout réduire au moins au rAF, car cela n'a pas de sens de demander une mise en page les modifications apportées par le navigateur plus souvent que le navigateur ne rend la mise en page.

Cependant, l'événement scroll est déclenché chaque fois que le navigateur rend un changement de position de défilement. Cela signifie qu'un événement scroll est synchronisé avec le rendu de la page. Littéralement la même chose que rAF vous donne. Cela signifie que cela n'a aucun sens d'étrangler quelque chose par quelque chose qui est déjà étranglé par exactement la même chose, par définition.

En pratique, vous pouvez vérifier ce que je viens de dire en ajoutant un console.log et vérifiez la fréquence à laquelle ce modèle "empêche plusieurs callbacks rAF" (la réponse est none, sinon ce serait un bug du navigateur).

  // Prevent multiple rAF callbacks.
  if (scheduledAnimationFrame){
    console.log('prevented rAF callback');
    return;
  }

Comme vous le verrez, ce code n'est jamais exécuté, il s'agit simplement d'un code mort.

Mais il existe un schéma très similaire qui a du sens pour une raison différente. Cela ressemble à ceci:

//declare box, element, pos
function writeLayout(){
    element.classList.add('is-foo');
}

window.addEventListener('scroll', ()=> {
    box = element.getBoundingClientRect();

    if(box.top > pos){
        requestAnimationFrame(writeLayout);
    }
});

Avec ce modèle, vous pouvez avec succès réduire, voire supprimer, les contusions de mise en page. L'idée est simple: à l'intérieur de votre écouteur de défilement, vous lisez la disposition et décidez si vous devez modifier le DOM, puis vous appelez la fonction qui modifie le DOM à l'aide de la fonction rAF. Pourquoi est-ce utile? Le rAF s'assure que vous déplacez l'invalidation de votre mise en page (à la fin du cadre). Cela signifie que tout autre code appelé à l'intérieur du même cadre fonctionne sur une présentation valide et peut fonctionner avec des méthodes de lecture de présentation très rapides.

Ce modèle est en fait si génial que je suggérerais la méthode d'assistance suivante (écrite 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

Est de l'API similaire à rAF mais donne quelque chose de totalement différent. Il vous donne des périodes d'inactivité à l'intérieur d'un cadre. (Normalement, le point après que le navigateur a calculé la disposition et fait Paint, mais il reste encore un peu de temps avant que la v-sync ne se produise.) Même si la page est lente de la vue des utilisateurs, il peut y avoir des cadres, où le navigateur est au ralenti. Bien que rIC puisse vous donner max. 50ms. La plupart du temps, vous ne disposez que de 0,5 à 10 ms pour remplir votre tâche. Etant donné le moment auquel le cycle de vie de la trame est appelé rIC, vous ne devez pas modifier le DOM (utilisez rAF pour cela).

À la fin, il est tout à fait judicieux d’étouffer l’écouteur scroll pour le lazyloading, le défilement infini, etc. avec rIC. Pour ces types d'interfaces utilisateur, vous pouvez même limiter davantage et ajouter un setTimeout devant celui-ci. (donc vous attendez 100ms puis un rIC) (exemple de vie pour un anti-rebond et accélérateur .)

Voici également un article sur rAF , qui inclut deux diagrammes qui pourraient aider à comprendre les différents points à l'intérieur d'un "cycle de vie de trame".

Auditeur d'événement passif

Les auditeurs d'événements passifs ont été inventés pour améliorer les performances de défilement. Les navigateurs modernes ont déplacé le défilement de page (rendu de défilement) du fil principal vers le fil de composition. (voir https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/ )

Mais il existe des événements qui produisent du défilement, ce qui peut être empêché par un script (ce qui se produit dans le thread principal et peut donc annuler l'amélioration des performances).

Ce qui signifie que dès que l'un de ces événements est lié aux écouteurs, le navigateur doit attendre que ces écouteurs soient exécutés avant de pouvoir calculer le défilement. Ces événements sont principalement touchstart, touchmove, touchend, wheel et théoriquement dans une certaine mesure keypress et keydown . L'événement scroll lui-même n'est pas l'un de ces événements. L'événement scroll n'a pas d'action par défaut, ce qui peut être empêché par un script.

Cela signifie que si vous n'utilisez pas preventDefault dans votre touchstart, touchmove, touchend et/ou wheel, utilisez toujours un événement passif. les auditeurs et vous devriez aller bien.

Si vous utilisez preventDefault, vérifiez si vous pouvez le remplacer par CSS touch-action ou abaissez-la au moins dans votre arborescence DOM (par exemple, aucune délégation d’événements pour ces événements). Dans le cas de wheel auditeurs, vous pourrez peut-être les lier/dissocier sur mouseenter/mouseleave.

En cas d'événement différent: il n'est pas judicieux d'utiliser des écouteurs d'événements passifs pour améliorer les performances. Le plus important à noter: L'événement scroll ne peut pas être annulé. Par conséquent, il jamais a du sens d'utiliser des écouteurs d'événements passifs pour scroll.

Dans le cas d'une vue de défilement infinie vous n'avez pas besoin de touchmove, vous avez seulement besoin de scroll, les écouteurs d'événements passifs ne s'appliquent donc même pas.

CV

Pour répondre à ta question

  • pour lazyloading, la vue infinie utilise une combinaison de setTimeout + requestIdleCallback pour vos écouteurs d'événement et de rAF pour toute écriture de mise en page (mutations DOM).
  • pour les effets instantanés, utilisez toujours rAF pour toute écriture de mise en page (mutations DOM).
98
alexander farkas

J'ai également vu quelques mentions d'utilisation de requestAnimationFrame non seulement pour étrangler, mais pour remplacer complètement l'écouteur de défilement.

Quelqu'un peut-il commenter cela? Je suppose que c'est une mauvaise pratique, car la lecture continuera en boucle, même si elle ne défile pas.

0