1. ホーム
  2. javascript

[解決済み] スクロールイベント:requestAnimationFrame VS requestIdleCallback VS パッシブイベントリスナー

2023-07-23 16:06:39

質問

ご存知のように、ユーザーがスクロールしているときにUXを良くするために、スクロールリスナーをデバウンスすることがよく勧められます。

しかし、私はしばしば ライブラリ 記事 では、Paul Lewis のような影響力のある人々が requestAnimationFrame . しかし、Web プラットフォームの急速な進歩に伴い、いくつかのアドバイスが時間の経過とともに非推奨となる可能性があります。

私が見た問題は、パララックスウェブサイトの構築、または無限スクロールとページネーションの処理のように、スクロールイベントを処理するための非常に異なるユースケースがあることです。

私は、UXの面で違いを生み出すことができる3つの主要なツールを見ています。

そこで、ユースケースごとに(私は2つしか持っていませんが、あなたは他のものを思いつくことができます)、非常に良いスクロール体験をするために、今どんなツールを使うべきかを知りたいのです。

より正確に言うと、私の主な質問は、無限スクロールのビューとページネーション(一般的に視覚的なアニメーションをトリガーする必要はありませんが、良いスクロール体験が必要です)に関連するもので、次のように置き換えるのが良いでしょうか? requestAnimationFrame のコンボで置き換える方が良いのでしょうか? requestIdleCallback + パッシブスクロールイベントハンドラ ? また、どのような場合に requestIdleCallback を使用して API を呼び出したり、API のレスポンスを処理してスクロールのパフォーマンスを向上させる意味があるのか、それともブラウザがすでに処理していることなのかについても疑問に思っています。

どのように解決するのですか?

この質問は少し古いのですが、これらのテクニックの多くが誤って使用されているスクリプトをよく見かけるので、回答したいと思います。

一般的に、あなたが尋ねたすべてのツール ( rAF , rIC やパッシブリスナー)は素晴らしいツールで、すぐになくなることはないでしょう。しかし、それらを使用する理由を知っておく必要があります。

始める前に。パララックス効果やスティッキー要素などのスクロール同期/スクロール連動効果を生成する場合、を使用してスロットリングを行います。 rIC , setTimeout は即座に反応したいので意味がない。

requestAnimationFrame

rAF は、ブラウザがドキュメントの新しいスタイルとレイアウトを計算しようとする直前の、フレームのライフサイクル内のポイントを提供します。これが、アニメーションに使用するのに最適な理由です。まず、ブラウザがレイアウトを計算する頻度(正しい頻度)よりも多く、または少なく呼び出されることはありません。次に、ブラウザがレイアウトを計算する直前に呼び出されます(適切なタイミング)。実際 rAF を使用することは、非常に理にかなっています。 rAF と同期して V-SYNC と同期されます。

を使って rAF スロットル/デバウンス用

Paul Lewisのデフォルトの例は、このようになります。

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

このパターンは非常によく使われ、コピーされますが、実際にはほとんど意味をなしません。(そして、なぜこの明らかな問題に気がつかないのか、私は自問しています。) 一般的に、理論的には、すべてを少なくとも rAF なぜなら、ブラウザがレイアウトをレンダリングするよりも頻繁にブラウザにレイアウトの変更を要求することは意味がないからです。

しかし scroll イベントが発生するたびに、ブラウザの レンダリング が発生するたびに発生します。これは scroll イベントがページのレンダリングと同期していることを意味します。文字通り、同じことを rAF が与えるものと文字通り同じものです。つまり、何かによって何かを調整することは意味がなく、それは定義に従ってまったく同じものによって既に調整されているのです。

実際には、私が今言ったことは console.log を追加し、このパターンがどれくらいの頻度で "複数の rAF コールバックを防止するか確認できます (答えは「なし」です。さもなければ、ブラウザのバグとなります)。

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

このコードは決して実行されず、単なるデッドコードであることがおわかりいただけると思います。

しかし、別の理由で意味をなす、非常によく似たパターンがあります。それは次のようなものです。

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

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

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

このパターンで、レイアウトの衝突をうまく減らすか、あるいは取り除くことができます。アイデアは簡単です。スクロールリスナーの内部でレイアウトを読み、DOMを修正する必要があるかどうかを決定し、次にrAFを使用してDOMを修正する関数を呼び出します。なぜこれが有用なのでしょうか?それは rAF は、レイアウトの無効化を(フレームのエンデッドで)移動させることを確認します。これは、同じフレーム内で呼び出される他のコードが有効なレイアウトで動作し、超高速のレイアウト読み取りメソッドで操作できることを意味します。

このパターンは実際とても素晴らしいので、次のヘルパーメソッド(ES5で書かれています)をお勧めします。

/**
 * From https://stackoverflow.com/a/44779316
 *
 * @param {Function} fn Callback function
 * @param {Boolean|undefined} [throttle] Optionally throttle callback
 * @return {Function} Bound 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;
  var that;
  var 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

APIからは、以下のような感じでしょうか。 rAF と似ていますが、全く異なるものを提供します。これは、フレーム内のアイドル時間を提供します。(通常、ブラウザがレイアウトを計算し、ペイントを行った後の時点ですが、V-Sync が発生するまでまだ時間が残っています)。 ユーザーから見てページが遅延していても、ブラウザがアイドル状態になっているフレームがあるかもしれません。しかし rIC は最大で 50msです。ほとんどの場合、0.5~10msの間にしかタスクを実行することができません。フレームのライフサイクルのどの地点で rIC コールバックが呼び出されるという事実があるため、DOM を変更してはいけません ( rAF を使います)。

最後にスロットルするのはとても理にかなっています。 scroll リスナーを調整することは、非常に理にかなっています。 rIC . このようなユーザーインターフェースの場合、さらにスロットルして setTimeout を追加することもできます。(つまり、100ms の待ち時間があり、その後に rIC )

のライブ例です。 デバウンス スロットル .)

以下は に関する記事もあります。 rAF についての記事もあります。この記事には、フレーム ライフサイクルのさまざまなポイントを理解するのに役立つ 2 つの図が含まれています。

パッシブイベントリスナー

パッシブ イベント リスナーは、スクロールのパフォーマンスを向上させるために考案されました。最近のブラウザでは、ページのスクロール (スクロール レンダリング) がメイン スレッドからコンポジション スレッドに移動しました。 (参照 https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/ )

しかし、スクロールを発生させるイベントがあり、これはスクリプトによって防ぐことができます (これはメインスレッドで発生するため、パフォーマンスの向上を元に戻すことができます)。

つまり、これらのイベント リスナーがバインドされるとすぐに、ブラウザはスクロールを計算する前に、これらのリスナーが実行されるのを待つ必要があります。これらのイベントは主に touchstart , touchmove , touchend , wheel であり、理論的にはある程度 keypress であり keydown . は scroll イベントそのものは ではなく のいずれかのイベントです。その scroll イベントにはデフォルトのアクションはなく、スクリプトによって阻止することができます。

これは、もしあなたが preventDefault を使わなければ touchstart , touchmove , touchend または wheel というように、常にパッシブなイベントリスナーを使用するようにすれば、問題ないでしょう。

preventDefaultを使用する場合、CSSで代用できるかどうか確認してください。 touch-action プロパティで代用できるか、少なくとも DOM ツリーでそれを下げられるかを確認してください (たとえば、これらのイベントに対してイベントデリゲーションを行わないなど)。の場合 wheel リスナーの場合、バインド/アンバインドは mouseenter / mouseleave .

その他のイベントの場合 パフォーマンスを向上させるために、受動的なイベントリスナーを使用することは意味がありません。最も注意すべきは scroll イベントはキャンセルすることができないので、それは は決して に受動的なイベントリスナーを使用することは理にかなっています。 scroll .

無限スクロールのビューの場合は touchmove は不要であり、必要なのは scroll だけなので、パッシブイベントリスナーは適用されません。

レジュメ

ご質問にお答えします。

  • 遅延ロードのために、Infinite Viewは以下の組み合わせを使用します。 setTimeout + requestIdleCallback をイベントリスナーに使用し rAF を使用してください(DOM 変異)。
  • インスタントな効果を得るには、まだ rAF を使用してください。