V-Syncとリフレッシュレートの呪縛:60Hzの壁を超え、ブラウザを極限まで滑らかにするアーキテクチャ
こんにちは。ブラウザのソースコードを眺めながらコーヒーを飲むのが日課のチーフアーキテクトだ。
日夜、数百万行のJavaScriptと格闘し、複雑なDOMツリーを構築している君なら、「なぜこれほどコードを最適化したのに、まだ画面がカクつくのか」という泥沼にハマった経験が一度はあるはずだ。
CPUのプロファイリングをしてもボトルネックは見当たらない。メインスレッドは暇そうにしている。それなのに、アニメーションが微妙に引っかかる。
犯人はコードの非効率さではない。ディスプレイの物理的な制約と、ブラウザのレンダリングパイプラインが織りなす「V-Sync(垂直同期)」の不可避な同調圧力だ。
今回は、このブラウザ描画の根本原理にメスを入れ、リフレッシュレートの罠を回避して「真の滑らかさ」を手に入れるためのアーキテクチャを深掘りしていこう。
—
1. V-Syncとは何か?:ピクセルを画面に叩き込むハードウェアの制約
まずは基本のおさらいだ。Webアプリケーションがいくら高度になろうとも、最終的にそれを映し出すのは物理的なディスプレイである。
ディスプレイは、上から下へ、左から右へ、猛烈なスピードで電子ビーム(あるいは液晶のピクセル制御)を走査して画面を描画している。この走査が1画面分完了し、次のフレームを描くためにビクセルが左上に戻る瞬間、それがV-Sync(垂直帰線期間)だ。
[ディスプレイの走査 (上から下へ)]
↓
[V-Syncパルス発生] ─── ここでフレームバッファをスワップする!
↓
[次の走査サイクルへ]
もし、このV-Syncのタイミングを無視して、ディスプレイが描画している最中に裏側のバッファ(フレームバッファ)の内容を書き換えてしまったらどうなるか?
画面の上半分は古いフレーム、下半分は新しいフレームという、見るに堪えない「ティアリング(画面のズレ)」が発生する。
これを防ぐために、ブラウザの合成エンジン(Compositor)は、ディスプレイのV-Sync信号とガッチリ同期してフレームを切り替えるよう設計されている。これがV-Syncの正体だ。安全な描画のための、絶対的な交通整理なのだ。
—
2. リフレッシュレートが課す「16.6ms(または8.3ms)の呪縛」
現代のディスプレイは多様化している。伝統的な60Hz(1秒間に60回更新、つまり1フレームあたり約16.6ms)だけでなく、ゲーミング向けの144Hz(約6.9ms)、さらにはMacBook Proなどが採用するProMotion(最大120Hz、約8.3ms)まで存在する。
ここで、ブラウザのレンダリングパイプラインが直面する残酷な現実がある。
1. V-Sync信号が来る
2. ブラウザが「よし、次のフレームを描くぞ」とメインスレッド(JS)やワーカースレッドを叩き起こす
3. スタイル計算、レイアウト、ペイント、そしてコンポジットが行われる
4. V-Syncの次のパルスが来るまでにGPUへフレームを送り、画面が切り替わる
もし、君の書いた複雑なUIコンポーネントのレイアウト計算が重すぎて、この16.6ms(60Hzの場合)のタイムリミットをわずかでもオーバーしたとしたらどうなるか?
60Hz環境 (1フレーム = 16.6ms)
[ V-Sync ] —> [ 処理遅延 (20ms) ] ——————> X (間に合わない!)
[ V-Sync ] [ V-Sync ] —> ここでようやく描画 (フレームドロップ!)
間に合わなかったフレームは容赦なくドロップされる。ディスプレイは前回のフレームをもう1回表示し続けるしかない。結果として、アニメーションは「カクッ」と停止する。これがスタッタリング(Stuttering)のメカニズムだ。
さらにタチが悪いのは、可変リフレッシュレート(VRR / G-Sync / FreeSync)環境下や、ブラウザがバックグラウンドタブに移行した時、あるいは省電力モードの時だ。ブラウザはデスクトップのコンポジターと協調しながら、このフレーム生成のタイミングを動的に調整し続けている。非同期処理の競合が起きやすいのは、まさにこの境界領域なのだ。
—
3. `requestAnimationFrame`の本当の役割と限界
フロントエンドエンジニアの必殺技といえば `requestAnimationFrame (rAF)` だ。「setTimeoutやsetIntervalはやめて、rAFを使え」と教わってきたはずだ。
なぜrAFなのか? それは、ブラウザのV-Syncサイクルに直結したタイミングでコールバックを実行してくれるからだ。
しかし、ここで上級エンジニアとして一歩踏み込んだ理解が必要になる。rAFは「魔法の滑らかさ製造機」ではない。
rAFのコールバック内でDOMの計測(強制同期レイアウトを引き起こすプロパティの読み取り)と、スタイル変更(書き込み)を混ぜこぜで行うと、いわゆる「レイアウト・スラッシング(Layout Thrashing)」を引き起こす。
// 【アンチパターン】レイアウト・スラッシングを誘発する悪夢のコード
requestAnimationFrame(() => {
// 1. DOMから高さを読み取る(ここでブラウザは強制的にレイアウトツリーを再構築する!)
const boxHeight = element.offsetHeight;
// 2. DOMのスタイルを書き換える
element.style.height = (boxHeight + 10) + ‘px’;
// 3. 再び読み取る
const nextWidth = anotherElement.offsetWidth; // 強制レイアウトの嵐!
});
このコードがrAFの周期(16.6ms以内)に収まらなくなった瞬間、フレームドロップの連鎖が始まる。rAFはあくまで「描画サイクルの起点」を教えてくれるだけであり、その中で何をするかはエンジニアのアルゴリズム設計にすべてがかかっているのだ。
—
4. パフォーマンス最適化:V-Syncの呪縛をいかにしてかわすか?
では、このハードウェアの制約とブラウザのレンダリング機構の中で、どうやって究極の滑らかさを担保するのか。実務で使える実践的なアプローチを見ていこう。
アプローチA: レンダリングの「オフフロード(Off-Screen)」
DOMの構築や重い描画処理をメインスレッドから完全に切り離す。特に`OffscreenCanvas`を用いたワーカースレッドでの描画は、メインスレッドのJS実行が詰まっても、アニメーションのコンポジットを独立して維持するための強力な武器だ。
アプローチB: データのバッチ処理と「読み取り/書き込みの分離」
レイアウト・スラッシングを防ぐためには、rAFのコールバック内での操作を厳格に分離する。最初にすべての「読み取り」を済ませ、その後にすべての「書き込み」をまとめる(Batched DOM Reads/Writes)。
以下に、その設計思想を取り入れた実用的なスケジューラのコード例を示す。
/
- 高度なフレーム・スケジューラ
- メインスレッドの競合を防ぐため、DOMの読み取りと書き込みのフェーズを厳密に分離する。
/
class FrameScheduler {
constructor() {
this.readQueue = [];
this.writeQueue = [];
this.isScheduled = false;
}
// 読み取りタスクの登録
scheduleRead(task) {
this.readQueue.push(task);
this.scheduleFlush();
}
// 書き込みタスクの登録
scheduleWrite(task) {
this.writeQueue.push(task);
this.scheduleFlush();
}
scheduleFlush() {
if (!this.isScheduled) {
this.isScheduled = true;
// V-Syncのタイミングに同期して実行を開始
requestAnimationFrame(() => this.flush());
}
}
flush() {
const reads = this.readQueue;
const writes = this.writeQueue;
this.readQueue = [];
this.writeQueue = [];
this.isScheduled = false;
// 1. フェーズ1: すべての読み取りを先に実行(レイアウトの強制再計算を1回に集約)
const readResults = reads.map(task => task());
// 2. フェーズ2: すべての書き込みを実行(スタイルの変更をまとめてバッチ処理)
writes.forEach((task, index) => task(readResults[index]));
}
}
// ─── 実使用例 ───
const scheduler = new FrameScheduler();
function updateUI(element) {
// 読み取りフェーズ
scheduler.scheduleRead(() => {
return element.getBoundingClientRect();
});
// 書き込みフェーズ(読み取り結果を受け取る)
scheduler.scheduleWrite((rect) => {
element.style.transform = `translateY(${rect.top + 10}px)`;
});
}
このパターンを徹底するだけで、無駄なリフロー(Reflow)の発生回数を劇的に減らし、V-Syncの16.6ms(または8.3ms)という厳しい制約の中に処理を収める確率を跳ね上げることができる。
—
5. チーフアーキテクトからの提言
Webブラウザは、単なる「ドキュメントのビューア」ではない。それは、OSの制約を背負いながら、ハードウェアのV-Syncとダイレクトに会話する極めて高度な仮想マシンだ。
リフレッシュレートの向上(120Hz、240Hzの世界)は、私たちフロントエンドエンジニアにとって福音であると同時に、「ごまかしが効かなくなった」という厳しいプレッシャーでもある。非同期処理の競合を見落とし、メインスレッドを安易にブロックするコードを書けば、ユーザーは即座に「ラグいアプリだ」と判断する。
ブラウザの内部構造、レンダリングのパイプライン、そしてV-Syncの呼吸を感じ取ること。それこそが、真に堅牢でストレスのないWebアプリケーションを作り上げる唯一の道なのだ。
さあ、プロファイラーを開き、君のアプリのフレームレートを確認してみよう。すべてのフレームが、美しくV-Syncの波に乗っていることを祈る。

コメント