[JavaScript] スクロールイベントを最適化してパフォーマンスを向上させる方法
ページのスクロールの滑らかさが損なわれると UX の低下に繋がる。
スクロールイベントの最適化
スクロールイベントは発生頻度が多いため、実装内容によっては Scroll Jank を引き起こす可能性がある。
- イベント内で
preventDefault()
が呼ばれている可能性がある({passive: false}
) - サイズや位置を取得する処理がある(対象 DOM の位置を取得するため
Forced Synchronous Layout
が発生する可能性がある)
参考 - What forces layout / reflow
これらの対策には、大量の処理イベントを間引く throttle が一般的である。
また、処理内でpreventDefault()
を呼ばないのであれば、Passive Event Listenerを利用することも可能である。
60fps と端末のリフレッシュ レート
今日の端末は、画面を 1 秒に 60 回リフレッシュする。そのため、アニメーションなどの実行中もしくはページのスクロール中は、端末のリフレッシュ レートに合わせて画面がリフレッシュ毎に 1 つの新しい画像またはフレームを表示する必要がある。
これらの間隔を数値にすると約 16 ミリ秒(1000 ミリ秒 / 60 = 約 16.66 ミリ秒)になる。この間隔に合致しない場合、フレームレートが低下し、ジャンクが発生する(画面上で描画が震えて見える)。
setTimeout()
これまで一般的に広くsetTimeout
が利用されてきた。次回の処理をスケジューリングし処理を頻繁に実行させないようにする事ができる。
- ブラウザ側の準備に関わらず必ず実行される
- タブが非アクティブ時でも実行される
関数が 16ms ごとに呼び出されないようにするには、以下のようにする。
var timer = null;
function func() {
clearTimeout(timer);
timer = setTimeout(function () {
// 処理
// do something
}, 16);
}
document.addEventListener("scroll", func, { passive: true });
lodash.throttle
lodash のthrottle
を使う手法もある。lodash.throttle - npm
import { throttle } from "lodash";
document.addEventListener("scroll", throttle(func, 16), { passive: true });
requestAnimationFrame()
requestAnimationFrame
は、処理を待つように時間指定するのではなく、次のフレームのレンダリングが準備が整った時に呼び出されるため、ほかの処理に割り込まれてフレームのレンダリングが遅延することなく適切なタイミングで呼び出される。
window.requestAnimationFrame - Web API インターフェイス | MDN
もちろん負荷の高い処理が重なると fps は落ちてしまう。あくまで次のフレームのレンダリング準備が整ったときに呼び出してくれるだけであり、どんな場合でも 60fps を保証する銀の弾丸ではない。
- ブラウザの画面リフレッシュと同じタイミングで呼び出される
- 画面が非アクティブ時には実行されない
requestAnimationFrame
を利用した throttle は、以下のようにする。
var ticking = false;
function func() {
if (!ticking) {
requestAnimationFrame(function () {
ticking = false;
// 処理
// do something
});
ticking = true;
}
}
document.addEventListener("scroll", func, { passive: true });
デモ
凝った実装ではないが、それぞれの処理を比較できるデモを用意した。
scroll 量に応じて指定要素のwidth
が変わる処理が走る。width
変更関数内にfor
で負荷をかけている。
function updateWidth(element) {
// 負荷をかけるループ
for (var i = 0; i < 100; i++) {
console.log("waiting...");
}
element.style.width = window.scrollY + 1 + "px";
}
setTimeout
は明らかに動作にもたつきが見られる。requestAnimationFrame
と_.throttle
(間隔 16.66 ミリ秒指定)には大きな差は見られないが、若干_.throttle
にチラつきが見れるケースもあった。
おわり
setTimeout
が安定しない場合はrequestAnimationFrame
を使うとほか処理の割り込みによる遅延を低減できる可能性が高まる。