Web●●●○○

ブラウザレンダリング

Rendering ・ ぶらうざれんだりんぐ

HTML・CSSが画面のピクセルになるまでの過程。この理解が表示速度の最適化を支える。

✦ 30秒で試す — 航海見た目は同じ「動かす」でも、重さは十倍以上違うページBで1フレームを描く時間は、Aと比べて?予想してみる

概要

ブラウザレンダリングは、サーバから届いた HTML と CSS という文字列が、画面上のピクセルとして表示されるまでの一連の処理過程です。ブラウザは受け取った文書を解析して木構造を組み立て、どの要素にどのスタイルが当たるかを決め、各要素の位置と大きさを計算し、色を塗り、最後に層を重ね合わせる — この流れは「レンダリングパイプライン」と呼ばれ、ページを開くたび、そして画面が変化するたびに動いています。

普段は意識しなくても動く仕組みですが、「ページの表示が遅い」「スクロールがカクつく」「操作してから反応するまでが重い」といった問題の原因は、ほぼ必ずこのパイプラインのどこかにあります。どの段階で何が起き、何がそれをブロックし、どの操作がどの段階からやり直しになるのか — この地図を持っているかどうかが、フロントエンドの速度改善の精度を分けます。

なぜ生まれたか

レンダリングの仕組み自体はブラウザの誕生とともにありましたが、開発者がそれを「知るべき知識」として学ぶようになったのは、Webがアプリケーション化してからです。静的な文書を表示するだけの時代は、描画は一度きりで終わり、速度はほぼ回線で決まりました。ところが JavaScript が DOM を頻繁に書き換え、アニメーションやスクロール連動が当たり前になると、描画は「毎秒60回締め切りが来る連続処理」になります。1フレームに使える時間は約16.7ミリ秒。この予算を超えた瞬間にカクつきとして体感される世界では、パイプラインのどの段階が高くつくかを知らずに書いたコードが、そのまま遅さになって現れます。

ブラウザの側もこの要求に合わせて作りを変えてきました。変更を溜めて1フレームに1回だけ描き直す、画面を層(レイヤ)に分けて GPU で重ね合わせ、動いた層だけを合成し直す、といった工夫です。そのおかげで「どう書けば安い段だけで済むか」に明確な答えが生まれ、開発者が意識すべき知識になりました。さらに、表示速度が検索順位や離脱率に直結することが数字で示されるようになり、GoogleはCore Web Vitalsという指標群でそれを検索評価に組み込みました。レンダリングの理解は、ブラウザ実装者の内輪の知識から、Web開発者の共通教養へと変わったのです。

詳細

パイプラインを順に追う

ブラウザはまず HTML を上から解析(パース)して DOM ツリーを構築します。並行して CSS を解析し、セレクタと宣言の集合である CSSOM(CSS Object Model)を作ります。ここまでは「木を2本作る」段階です。

次のスタイル計算で、2本の木が出会います。DOM の要素1つずつについて、当たる CSS の規則を集め、カスケードで勝者を決め、継承を解決して、最終的な値(計算値)を確定させます。この結果、実際に表示される要素と、その要素に当たるスタイルの組ができます(教科書的には「レンダーツリー」と呼ばれる段階です)。display: none の要素はここで表示対象から外れ、以降の工程から完全に抜けます。

その先のレイアウトでは、各要素の位置と大きさをビューポート基準で計算します。続くペイントで文字・色・影などを描画命令に変換し、最後のコンポジット(合成)で、複数のレイヤを GPU で重ね合わせて1枚の画面に仕上げます。

HTMLCSSパースパースDOM ツリーCSSOMスタイル計算要素ごとに当たる値を確定レイアウト位置と大きさを計算ペイント色や文字を描画命令にコンポジットレイヤを GPU で合成width・top・left・font-sizecolor・background・box-shadowtransform・opacity左の段から再実行を招く変更ほど高くつく。何を変えると、どの段からやり直しになるかがチューニングの鍵
レンダリングパイプライン。2本の解析がスタイル計算で合流し、レイアウト → ペイント → コンポジットと進む。下の注記は、そこから再実行を招く変更の例

レンダリングブロッキング — 最初の表示を止めるもの

この最初の1回のパイプライン、すなわちクリティカルレンダリングパスを最短にすることが初期表示の最適化です。ここで問題になるのがブロッキングです。CSS は「CSSOM が完成するまでスタイル計算ができない」ためレンダリングブロッキングであり、<head> 内の同期的な <script> は「DOM を書き換えるかもしれない」ためパース自体を止めます。だからこそ、CSSは可能な限り小さく早く届け、JavaScript には defer や async を付けてパースを止めない、ファーストビューに必要な最小限のCSSだけをインライン化する、といった定石が生まれました。SSR や SSG が「最初から描画可能なHTMLを返す」ことで初期表示に強いのも、このパスを短縮しているからです。

リフローとリペイント — 変更のコストは段で決まる

描画後に画面を変更するとき、パイプラインの「どこからやり直すか」でコストが桁違いに変わります。要素の幅・高さ・位置など形に関わる変更はレイアウトからやり直しになり、これをリフロー(再レイアウト)と呼びます。ある要素のサイズ変更が親や兄弟の配置に波及するため、ページが大きいほど高くつきます。一方、色や影など形を変えない変更はペイントからのやり直し(リペイント)で済み、リフローよりは軽い。そして transform や opacity の変更は、その要素が独立したレイヤになっていればコンポジットだけで処理でき、最も安価です。アニメーションを left を動かして作るとカクつくのに transform: translateX なら滑らかなのは、前者が毎フレームリフローを起こし、後者は合成だけで済むからです。

もうひとつの古典的な罠が「強制同期レイアウト」です。ブラウザは本来、JavaScript が加えた変更を溜めておき、フレームの終わりにまとめてレイアウトします。ところが JavaScript でスタイルを書き換えた直後に offsetHeight や getBoundingClientRect() などレイアウトの結果を読むと、ブラウザは正確な値を返すために、その場でレイアウトを実行させられます。読み書きをループ内で交互に行うと「レイアウトスラッシング」と呼ばれる連続リフローになり、一気に性能が崩れます。対策は、読みをまとめて先に済ませてから書く(あるいは書きを requestAnimationFrame に寄せる)ことです。

16.7ms の締め切りleft で動かす毎フレーム レイアウトから。ページが重いと締め切りを超えるtransformレイアウトとペイントを飛ばして合成だけ書いては読む読むたびに強制レイアウト。フレームが落ちるJavaScriptスタイルレイアウトペイント合成
1フレーム 16.7ms の予算の使い方。同じ「箱を動かす」でも、どの段から再実行するかで予算の減り方がまったく違う

下の部品で確かめてください。画面の箱の位置・幅・色を直接つかんで変えると、パイプラインのどの段が再実行されるかが光り、1フレームの予算バーが埋まっていきます。ページの重さを上げたり、書くたびに offsetHeight を読むスクリプトをはさんだりすると、何が締め切りを超えさせるのかが見えます(段ごとの時間は典型的な比率の模型です。「本物のブラウザで計る」では、この端末での実際の強制レイアウトの時間を測ります)。

⚡ 体験: 箱を動かして、どの段から描き直しが走るかを見る
画面(ほか 600 要素のページ)色の帯箱をつかむ=位置 / 右端=幅箱JavaScript—スタイル計算—レイアウト—ペイント—合成—変えたもの: —16.7ms の締め切り直近のフレーム(朱=締め切り超え )16.7ms
ページの重さ
位置の動かし方
スクリプト
JavaScriptスタイル計算レイアウトペイント合成

画面の箱をつかんで動かす・右端を引いて幅を変える・上の色の帯をなぞるのどれかをしてみてください。どの段から描き直しが走るかが下に光ります。

GPU合成レイヤと Core Web Vitals

コンポジット工程では、ブラウザは一部の要素を独立した合成レイヤに昇格させます。レイヤは GPU 上のテクスチャとして保持されるため、位置や透明度の変更は再描画なしで合成し直すだけで反映できます。CSS のアニメーションやトランジションで transform・opacity を動かす場合は、合成を担う別のスレッドで進められるので、JavaScript で混み合ったメインスレッドが止まっていても滑らかに動き続けます。will-change などでレイヤ化を促せますが、レイヤはメモリを食うため乱用は逆効果 — 「動かすものだけ昇格させる」のが原則です。

こうした知識を実務の目標値に落とし込んだのが Core Web Vitals です。LCP(Largest Contentful Paint)は主要コンテンツの表示までの速さでクリティカルレンダリングパスの成績を、INP(Interaction to Next Paint。2024年に FID に代わって採用)は操作してから次の描画までの遅れでメインスレッドの詰まりを、CLS(Cumulative Layout Shift)は表示中のレイアウトのずれ、つまり意図しないリフローの積み重ねを測ります。改善の手筋 — 画像への寸法指定でCLSを防ぐ、CDN とキャッシュで取得を速めてLCPを縮める、長い JavaScript のタスクを分割してINPを守る — は、どれもこのパイプラインの地図の上に載っています。計測には Chrome DevTools の Performance パネルと Lighthouse が定番で、推測ではなく記録されたフレームを見て直すのが鉄則です。

試問

画面の模型に箱が1つあり、ほかに 2,500 個の要素が並ぶ重いページです。箱はつかんで動かせ、位置の変え方(transform か left)を切り替えられます。下には、パイプラインのどの段が走ったかと、1フレームの時間が締め切り 16.7ms を超えたかが出ます。

✦ 命題挑戦中

2,500 要素の重いページで、フレームを1つも落とさずに(締め切り 16.7ms を超えずに)箱を大きく移動させよ

  • 重いページ(2,500 要素)で、箱をつかんで大きく動かす
  • その動きのあいだ、締め切り(16.7ms)を超えたフレームが 0
⚡ 体験: 箱を動かして、どの段から描き直しが走るかを見る
画面(ほか 2,500 要素のページ)色の帯箱をつかむ=位置 / 右端=幅箱JavaScript—スタイル計算—レイアウト—ペイント—合成—変えたもの: —16.7ms の締め切り直近のフレーム(朱=締め切り超え )16.7ms
位置の動かし方
スクリプト
JavaScriptスタイル計算レイアウトペイント合成

画面の箱をつかんで動かす・右端を引いて幅を変える・上の色の帯をなぞるのどれかをしてみてください。どの段から描き直しが走るかが下に光ります。