requestAnimationFrameで実現する、スクロール方向に応じたUI切り替え
スクロールの上下方向に応じて、UIの見た目を変化させたい場面は意外と多くあります。
その都度スクロール方向を判定する方法もありますが、body
要素にカスタムデータ属性でグローバルにスクロール方向を保持するという手法が、複数箇所で使いまわせて便利だと最近気がつきました。
コードの全体像
さっそくですが、スクロール方向をdata-scroll-direction
というカスタムデータ属性で管理するコード例を紹介します。
const initScrollDirectionDetection = () => { let lastScrollY = window.scrollY; let isScheduled = false;
const updateScrollDirection = () => { const currentScrollY = window.scrollY; const direction = currentScrollY > lastScrollY ? "down" : "up";
if (document.body.dataset.scrollDirection !== direction) { document.body.dataset.scrollDirection = direction; }
lastScrollY = currentScrollY; isScheduled = false; };
window.addEventListener("scroll", () => { if (!isScheduled) { requestAnimationFrame(updateScrollDirection); isScheduled = true; } });};
initScrollDirectionDetection();
スクロール方向に応じて、body
要素のdata-scroll-direction
属性が次のように動的に変化します:
- 下方向にスクロール:
down
- 上方向にスクロール:
up
ヘッダーをスクロール方向に応じて表示/非表示を切り替える
実務でよくあるユースケースとして、「スクロール方向に応じてヘッダーを見え隠れさせる」といった実装があります。
以下の例では、下方向にスクロールするとヘッダーが非表示になり、上方向にスクロールすると再び表示されるという動きを実現しています。
この挙動は、スクロール時にbody
のdata-scroll-direction
に応じて、CSSで.l-header
のtransform
の値を変更することで実現しています。
/* スクロール時 */body[data-scroll-direction="down"] .l-header { transform: translateY(calc(-100% - 1px));}
このような実装は、ヘッダーが比較的大きい場合や、コンテンツの閲覧領域を少しでも広く確保したい場合に有効だと感じます。
requestAnimationFrameとは
今回の実装ではrequestAnimationFrame
を使用しています。
時々見かけるけどもあまり深くまで理解できていないメソッドの代表格ではないでしょうか。
MDNでは以下のように説明されています。
ブラウザーにアニメーションを行いたいことを知らせ、指定した関数を呼び出して次の再描画の前にアニメーションを更新することを要求します。
つまり、次の再描画の直前にコールバック関数を実行してくれる仕組みです。
スクロール処理での動き
次のコードでは、一見すると6行目でisScheduled = true;
にしているため、2回目以降のスクロールではrequestAnimationFrame
は実行されないのでは?と思うかもしれません。
let isScheduled = false;
window.addEventListener("scroll", () => { if (!isScheduled) { requestAnimationFrame(updateScrollDirection); isScheduled = true; }});
しかし実際には2回目以降のスクロールでも適切にコールバックが登録されて実行されます。
その理由はrequestAnimationFrame
のコールバック内でフラグをリセットしているからです。以下のような順序で処理が実行されます。
1回目のスクロール: |
|
---|---|
2回目のスクロール (同一フレーム内): |
|
2回目のスクロール (次のフレーム): |
|
行ごとの処理を追うと、実行タイミングのイメージは以下のようになります。ポイントとしてはupdateScrollDirection
の実行タイミングが次のフレームの直前である点です。
window.addEventListener("scroll", () => { if (!ticking) { // ① requestAnimationFrame(updateScrollDirection); // ② ticking = true; // ③ }});
const updateScrollDirection = () => { // ... DOM更新処理 ... ticking = false; // ④ ここでフラグをリセット!}
以上のことからrequestAnimationFrame
はコールバック関数を登録し、すぐには実行せず次の再描写の直前に処理が行われることがわかります。
requestAnimationFrameを使用するメリット
ではrequestAnimationFrame
を使うと何が良いのか?
ここでは単純にスクロールイベントのコールバック関数としてupdateScrollDirection
をそのまま渡した例と比較してみます。
window.addEventListener("scroll", updateScrollDirection);
requestAnimationFrameなし | requestAnimationFrameあり |
---|---|
|
|
高速スクロール時の実行回数が、この例では約13分の1まで削減できています。つまり、数百〜数千回発生していた処理を、最大でも60回程度にまで抑えることができ、ページ全体のパフォーマンス向上に直結するようです。
私自身、requestAnimationFrame
を完全に理解したとは言えませんが、今回の実装を通じて「何ができるのか」「どう使えば良いのか」の感覚がつかめた気がします。
スクロールイベントの最適化や、DOM操作の負荷軽減に今後もぜひ活用してみたいです。