requestAnimationFrameを用いたスクロールイベントの描画最適化
JavaScript Performance Frontend DOM
結論
ticking フラグを使い、DOM描画を requestAnimationFrame に委譲してレイアウトの再計算を1フレーム1回に抑制します。
// MDN公式仕様:requestAnimationFrame を用いた最適化パターン
let lastScrollY = 0;
let ticking = false;
function updateHeader(scrollY) {
const header = document.querySelector('header');
if (scrollY > 100) {
header.classList.add('is-scrolled');
} else {
header.classList.remove('is-scrolled');
}
}
window.addEventListener('scroll', () => {
lastScrollY = window.scrollY; // Read Phase (イベント内)
if (!ticking) {
window.requestAnimationFrame(() => {
updateHeader(lastScrollY); // Write Phase (rAF内)
ticking = false;
});
ticking = true;
}
});
パフォーマンス比較
| 手法 | リフレッシュレート同期 | 描画実行頻度 | CPU負荷 / 画面カクツキ |
|---|---|---|---|
| スクロール直呼び出し | ❌ 非同期 | 1秒間に 100回以上高頻度実行 | ❌ 高負荷・重度のJank発生 |
setTimeout スロットル | ❌ ディスプレイ描画とズレる | 固定ミリ秒指定 | △ コマ落ちの可能性あり |
requestAnimationFrame | ◯ ディスプレイ(60Hz/120Hz)と完全同期 | 1フレーム(16.6ms)に最高1回 | ◯ 滑らかで超低負荷 |
実際に起こる障害:レイアウトスラッシング (Layout Thrashing)
MDN公式ドキュメント(Rendering Performance)で警告されている通り、スクロールイベントハンドラ内で DOMの読み取り(Read)とDOMの書き込み(Write)を交互に呼び出すコード を書くと、ブラウザは1フレーム中に何度も同期再計算(Reflow)を強制実行させられます。
// ❌ 重度のレイアウトスラッシングを起こすダメな例
window.addEventListener('scroll', () => {
// Read
const height = element.offsetHeight;
// Write (ここでブラウザのレイアウトキャッシュが破壊される)
element.style.height = height + 1 + 'px';
// 再度 Read (強制再レイアウトが発生!)
const nextTop = element.getBoundingClientRect().top;
});
実装手順
- ループ状態を保持する
ticking = falseフラグ変数を用意する - イベント発火時に
window.scrollYなどの位置情報(Read)を変数にのみ保持する tickingがfalseの時だけrequestAnimationFrameを予約し、そのコールバック内でクラス付与やスタイル変更(Write)を行う- 処理完了時に
ticking = falseに戻す