お疲れ。最近、フロントエンドのコードレビューをしていて「うわ、またやってるな」って思う瞬間があるんだよね。何だと思う?
そう、アニメーションやスクロール位置の監視のために、平然と `setInterval` や `setTimeout` を使っているコードさ。動けばいいって時代はとうの昔に終わった。60fps(あるいはハイリフレッシュレートなら120fps)の滑らかなUI体験が当たり前の今、タイマー任せの描画はカクつき(Jank)の元凶になる。
今回は、ブラウザの心臓部である「レンダリングループ」と、そこに完璧に同調するための最強の武器`requestAnimationFrame`(rAF)の仕組みについて、裏側の挙動を含めて徹底的に叩き込んでおく。中級から一段上のシニアへステップアップしたいなら、この領域の解像度を上げておくことは必須だ。
—
1. ブラウザの「心臓音」:レンダリングループの正体
まず、ブラウザが画面をどのように描いているか、その裏側のタイムラインをイメージできているかい?
多くの人は「JavaScriptが動いたら、即座に画面が書き換わる」と思っている。大間違いだ。JavaScriptの実行、スタイルの計算、レイアウト、ペイント……これらはすべて、ブラウザのメインスレッド上で順番に行われている。
一般的なディスプレイの更新頻度は 60Hz(1秒間に60回)。つまり、ブラウザに残された時間は1フレームあたりわずか「約16.67ミリ秒」しかない。この16.67msの中に、JSの処理もレンダリングも全て収めなきゃいけないわけだ。
ブラウザの内部で起きていること(1フレームのライフサイクル)
ブラウザの裏側では、だいたい次のようなサイクル(Tick)が常に高速で回っている。
1. 入力イベントの処理(Input Events): ユーザーのタッチやスクロール、クリックのハンドリング。
2. タイマーの実行(Timers): `setTimeout` や `setInterval` のコールバック処理。
3. ネットワークイベント等の処理: 非同期通信の完了ハンドラなど。
4. `requestAnimationFrame` の実行(★ここ!): 次の描画直前に実行されるコールバック。
5. レイアウト / リフロー(Layout): 要素のサイズや位置の計算。
6. ペイント(Paint / Composite): ピクセルを画面に描き出す処理。
ここで重要なのは、`setTimeout` はブラウザの描画タイミングなどお構いなしに発火するという点だ。だから、16.67msのサイクルのどの変なタイミングで割り込むか分からない。最悪の場合、フレームの真ん中でJSが DOMを書き換えてしまい、同じフレーム内で無理やり再計算(強制同期レイアウト)が走ることで、フレームレートがガタ落ちする(Jankの発生)。
そこで登場するのが、ブラウザの更新サイクルにピタリと寄り添う `requestAnimationFrame` なんだ。
—
2. `requestAnimationFrame` はなぜ「神」なのか?
`requestAnimationFrame` の本質は、「ブラウザが次に画面を再描画(リフレッシュ)する直前に、この処理を実行してくれ」と予約するAPIだ。
メリットを挙げていこう。
- 描画サイクルへの完全な同期: ディスプレイの更新タイミングに合わせるため、無駄な描画(1フレーム中に2回以上DOMをいじるなど)が起きない。
- バックグラウンドタブでの自動停止: ユーザーが別のタブに移動したり、ウィンドウを最小化したりすると、ブラウザは自動的にrAFの実行頻度を落とす(または止める)。これでCPU/GPUの無駄なバッテリー消費を防げる。`setInterval` だとバックグラウンドでも狂ったように動き続けてファンが唸るよな?あれを防げる。
- 複数アニメーションのバッチ処理: 同一フレーム内で複数のrAFが登録されていても、ブラウザはそれらをまとめて効率よく処理してくれる。
—
3. 実践:レイアウトスラッシングを防ぐループ構造
実務でよくあるアンチパターンが、rAFの中で無造作にDOMの「読み取り(Read)」と「書き込み(Write)」を交互に繰り返すことだ。これだと、ブラウザのレイアウトエンジンが何度も計算をやり直すことになり、パフォーマンスが死ぬ(通称:レイアウトスラッシング)。
シニアとして、「読み取りは一気に、書き込みも一気に」まとめるバッチ処理のパターンを体に叩き込んでおこう。
以下のコードは、スクロール位置に応じて要素の位置を滑らかに追従させる、現場でそのまま使える実用的なサンプルだ。
/
- 高パフォーマンスなスクロール&アニメーション制御クラス
- レイアウトスラッシングを回避し、rAFで完全に同期させる実装例
/
class SmoothScroller {
constructor() {
this.ticking = false;
this.lastScrollY = 0;
// 対象の要素を取得
this.targetElement = document.querySelector(‘.js-parallax-target’);
if (!this.targetElement) return;
this.init();
}
init() {
// スクロールイベントは頻繁に発火するため、リスナー内では重い処理をしない
window.addEventListener(‘scroll’, () => {
this.lastScrollY = window.scrollY;
this.requestTick();
}, { passive: true }); // passive: true でスクロールのパフォーマンスを担保
}
requestTick() {
// すでにリクエストが予約されている場合は、重複して予約しない(スロットリングの役割)
if (!this.ticking) {
requestAnimationFrame(() => {
this.update();
this.ticking = false; // 次のフレームのためにフラグをリセット
});
this.ticking = true;
}
}
update() {
// — 【Phase 1: 読み取り(Read)】 —
// DOMのレイアウトプロパティへのアクセスは、このフェーズでまとめて行う
const viewportHeight = window.innerHeight;
// — 【Phase 2: 書き込み(Write)】 —
// 読み取った値を元に、スタイルをまとめて適用する(ここでレイアウトを強制発動させない)
const translateY = this.lastScrollY 0.3; // 視差効果の計算
// transform を使ってGPUアクセラレーションを効かせる(topやleftは絶対に使わない!)
this.targetElement.style.transform = `translate3d(0, ${translateY}px, 0)`;
}
}
// インスタンス化して実行
const scroller = new SmoothScroller();
このコードの泥臭いポイント解説
1. `{ passive: true }` の指定: スクロールイベントでこれをつけるのは現代のフロントエンドの常識だ。ブラウザに対して「このリスナー内で `preventDefault()` は呼ばないから、スクロールを止めるなよ」と事前に伝えることで、スクロールの滑らかさが段違いになる。
2. `this.ticking` によるガード: 1フレームの間にスクロールイベントが何十回も発火しようとも、rAFの予約は1回だけに制限している。無駄な関数呼び出しのオーバーヘッドを徹底的に削るプロの技だ。
3. `translate3d` の採用: DOMの位置を変えるときに `top` や `left` をいじると、ブラウザは「Layout(リフロー)」からやり直すことになる。しかし `transform: translate3d()` なら、コンポジットレイヤーでの処理になるため、GPUが爆速で処理してくれる。アニメーションは原則これ一択だ。
—
4. まとめ:ブラウザと「対話」せよ
フロントエンド開発の本質は、ブラウザという限られたリソースの仮想マシンと、いかに上手く対話するかだ。
「とりあえず動くから `setInterval` でいいや」ではなく、「今、ブラウザのメインスレッドはどういう状態にあるか? 次の16.67msに間に合うか?」という視点を持てるかどうかが、中級から一流のエンジニアへの分かれ道になる。
アニメーションや連続するUI更新を実装するときは、まず頭の中でブラウザのレンダリングループを思い描き、`requestAnimationFrame` をスマートに組み込んでほしい。ユーザーがストレスを感じない、吸い付くような滑らかなUIを一緒に作っていこうぜ。

コメント