【テクニカル・上級編】 requestAnimationFrameの実行タイミングとレンダリング同期 – Webブラウザの仕組み実践ガイド

ブラウザの深淵:`requestAnimationFrame`とレンダリングパイプラインの同期戦略

フロントエンドの戦場において、「フレーム落ち」は単なる不具合ではない。それはユーザーの脳が感じる「違和感」という名の死刑宣告だ。60fps(あるいは120fps)というシビアな制限の中で、我々エンジニアはブラウザという巨大なブラックボックスと常に同期を競っている。

今回は、多くのエンジニアが「なんとなく」使っている `requestAnimationFrame` (rAF) の本質を解剖し、レンダリングパイプラインの深淵に触れていこう。

ブラウザの描画サイクルとrAFの「特権」

まず、ブラウザのメインスレッドがどう動いているかを想像してほしい。HTMLパース、JavaScriptの実行、スタイル計算、レイアウト、ペイント、そして合成(Compositing)。これらは決してランダムに動いているわけではない。

rAFは、ブラウザが「次の画面を再描画する直前」に実行される特権的なコールバックだ。`setTimeout(fn, 0)` と決定的に違うのは、「描画ステップに同期している」という点だ。

  • setTimeout/setInterval: タスクキューに積まれる。メインスレッドが空いていれば即座に実行されるが、描画サイクルとは無関係。最悪の場合、描画の合間に滑り込んでしまい、無駄な再描画を誘発する。
  • requestAnimationFrame: ブラウザのレンダリングサイクル(いわゆるV-Sync)に最適化されたタイミングで呼び出される。これにより、描画の直前にDOMを操作し、一回の描画サイクルで滑らかなアニメーションを実現できる。

レンダリングパイプラインの「裏側」を制御する

我々が真に意識すべきは、rAFが呼び出されるタイミングだ。ブラウザのライフサイクルは以下のように流れる。

1. 入力イベントの処理
2. JavaScript実行(タスク)
3. requestAnimationFrameの実行
4. スタイル計算(Recalculate Style)
5. レイアウト(Layout/Reflow)
6. ペイント(Paint)
7. 合成(Composite)

ここで重要なのは、rAF内でDOMを操作すると、直後のステップで「強制的なスタイル再計算」や「レイアウト」が走るという点だ。もしrAF内で重たい計算をしてしまうと、このパイプライン全体が詰まり、結果としてフレームがドロップする。

パフォーマンス最適化:読み取りと書き込みの分離

多くの開発者が陥る罠が、rAF内での「読み取り(Layout Thrashing)」と「書き込み」の混在だ。

// 悪い例:レイアウト・スラッシングの温床
function update() {
requestAnimationFrame(() => {
// 読み取り(ブラウザは最新のレイアウトを強制的に計算する)
const height = element.offsetHeight;

// 書き込み(レイアウトが崩れるため、再度計算が発生する)
element.style.height = (height + 10) + ‘px’;
});
}

これを防ぐには、「読み取りフェーズ」と「書き込みフェーズ」を明示的に分けるというアーキテクチャが必要だ。

// 良い例:FastDOM的なアプローチ
function optimizedUpdate() {
requestAnimationFrame(() => {
// 1. 読み取りフェーズ:DOMの状態を取得するだけ
const height = element.offsetHeight;

// 2. 書き込みフェーズ:次回の描画サイクルに向けて変更を予約する
// 必要であれば、次のrAFやmicrotaskで書き込む
requestAnimationFrame(() => {
element.style.height = (height + 10) + ‘px’;
});
});
}

非同期の競合と重大なバグの回避策

複数のアニメーションやデータフェッチが絡む複雑なアプリケーションでは、rAFのコールバックが競合することがある。ここで発生するのが「状態の不整合」だ。

特に気をつけたいのは、ReactやVueのようなフレームワークを使っている場合だ。フレームワーク自体も内部でrAFや `queueMicrotask` を駆使してDOM更新をスケジューリングしている。これと自前のrAFが衝突すると、予期せぬ再レンダリングや、古い状態を参照するバグが発生する。

解決のヒント:
複雑な状態同期が必要な場合は、`requestAnimationFrame` を直で叩くのではなく、「状態のクリーンアップ」を管理するラッパーを作ることだ。

// シンプルなスケジューラーの雛形
class FrameScheduler {
constructor() {
this.pending = false;
}

// 複数の要求があっても、次のフレームで一度だけ実行させる
schedule(callback) {
if (this.pending) return;
this.pending = true;

requestAnimationFrame(() => {
callback();
this.pending = false;
});
}
}

最後に:ブラウザを愛するということ

ブラウザは究極の「動的環境」だ。ネットワークの遅延、CPUの負荷、ユーザーの操作。これらすべての不確定要素を乗り越えて、60fpsを叩き出すのは、まさに職人の領域だ。

rAFを使いこなすということは、ブラウザの描画エンジンであるBlinkやWebKitと会話するようなものだ。内部のフローを理解し、パイプラインのボトルネックを見抜き、無駄な計算を削ぎ落とす。

公式ドキュメントには載っていない、こうした「泥臭い最適化の勘所」こそが、堅牢で滑らかなWebアプリケーションを生み出す唯一の道だと私は信じている。次はぜひ、Chrome DevToolsの「Performance」タブを開き、あなたのコードがどのようにパイプラインを占有しているのか、その深淵を覗いてみてほしい。そこには、驚くほど正直なブラウザの姿があるはずだ。

コメント

タイトルとURLをコピーしました