ブラウザのレンダリングの仕組み
受け取った HTML と CSS を、ブラウザが DOM・CSSOM →レンダーツリー→レイアウト→ペイント→合成と段階的に処理して、最終的にピクセルとして画面に描き出すまでの流れ。
- 1.ブラウザは HTML→DOM、CSS→CSSOM を作り、両者を合わせたレンダーツリーから レイアウト→ペイント→合成 の順で画面を描く(クリティカルレンダリングパス)。
- 2.位置やサイズが変わると レイアウトからやり直す“リフロー”、見た目だけなら“リペイント”。リフローは重いので避け、できれば transform/opacity の合成だけで動かす。
- 3.`<script>` は標準でパースを止める。外部 JS は基本 defer、依存しない計測系などは async。CSS は早く、JS は遅らせるのが高速化の基本。
全体の流れ
最初の表示(ファーストビュー)までに、ブラウザは大きく5つの工程を踏みます。
- パース → DOM:HTML を解析し、要素の親子関係を表すツリー(DOM)を作る。
- パース → CSSOM:CSS を解析し、各要素にどのスタイルが効くかのツリー(CSSOM)を作る。
- レンダーツリー構築:DOM と CSSOM を合わせ、実際に画面に出る要素だけを集めたツリーを作る。
- レイアウト(リフロー):各要素の位置と大きさを、ビューポートを基準に計算する。
- ペイント → 合成:ピクセルを塗り(ペイント)、複数のレイヤーを重ね合わせて(合成)最終画面を作る。
ポイントは、この順番は基本的に飛ばせないこと。位置が決まっていない要素を塗ることはできないので、レイアウトの前にペイントは来ません。
DOM:HTML から作る要素のツリー
ブラウザは HTML を上から読み(パースし)、<html> を頂点とする親子関係のツリー= DOM(Document Object Model) に変換します。タグの開閉を解釈して入れ子構造を組み立て、文字はテキストノードになります。
<body>
<h1>Hello</h1>
<p>世界よ <strong>こんにちは</strong></p>
</body>
この HTML は「body の下に h1 と p、p の下にテキストと strong」という木構造の DOM になります。JavaScript から document.querySelector などで触れるのは、この DOM です。
<div>...</div> というテキストが HTML、それをブラウザが解釈して作るメモリ上のオブジェクトツリーが DOM です。JS で element.textContent を書き換えると変わるのは DOM のほうで、サーバ上の元 HTML ファイルは変わりません。詳しくは DOM の解説へ。
CSSOM:CSS から作るスタイルのツリー
並行して、ブラウザは CSS を解析して CSSOM(CSS Object Model) を作ります。これは「どの要素に、最終的にどんなスタイルが当たるか」を計算したツリーです。継承(親の color を子が引き継ぐ)や、詳細度(#id と .class のどちらが勝つか)の解決もここで行われます。
CSS は レンダリングをブロックするリソースです。スタイルが未確定のまま描画すると、いわゆる「スタイル崩れの一瞬(FOUC)」が起きるため、ブラウザは CSSOM が出来上がるまで描画を待ちます。だから CSS はできるだけ早く・小さく届けるのが鉄則です。
レンダーツリー:実際に“描かれる”ものだけ
DOM と CSSOM を合わせて レンダーツリー を作ります。重要なのは、DOM とレンダーツリーは一致しないことです。
display: noneの要素は レンダーツリーに載らない(場所も取らず、描かれない)。<head>や<script>、メタ情報など画面に出ないノードも載らない。visibility: hiddenは載る(見えないが場所は取る)点がdisplay: noneとの違い。
| 指定 | レンダーツリー | 場所(レイアウト) | 見た目 |
|---|---|---|---|
| display: none | 載らない | 取らない | 消える |
| visibility: hidden | 載る | 取る | 見えない(空白が残る) |
| opacity: 0 | 載る | 取る | 透明(クリックは効く) |
レイアウト → ペイント → 合成
レンダーツリーが出来たら、いよいよ画面化です。ここが本記事の心臓部です。
レイアウト(別名:リフロー)
各要素が ビューポート上のどこに・どれだけの大きさで収まるかを計算します。width: 50% のような相対値を実際のピクセルに直し、ボックスの座標を確定させる工程です。ウィンドウ幅が変わると全体を計算し直すため、ここは重い処理になりがちです。
ペイント(ラスタライズ)
確定した各ボックスを、実際のピクセルに塗ります。文字・色・影・枠線・画像などを、レイヤーというキャンバスに描き込む工程です。
合成(コンポジット)
ページは1枚絵ではなく、複数のレイヤーに分かれて描かれることがあります(position: fixed の要素や、transform を持つ要素などが別レイヤーになりやすい)。それらを正しい重なり順で合成して、最終的に画面へ出します。この合成は GPU が担当することが多く、非常に高速です。
transform や opacity の変化は、レイアウトもペイントもやり直さず、合成のステップだけで済むことが多いです。アニメーションを left/top/width ではなく transform: translate() / scale() で行うと、毎フレームのリフロー・リペイントを避けられ、滑らか(60fps)になりやすい——これが「transform で動かせ」と言われる理由です。
リフロー と リペイント
一度表示した後も、画面は更新され続けます。何が変わったかによって、やり直す工程が変わります。
- リフロー(reflow):要素の位置・サイズが変わったとき。レイアウトからやり直すので最も重い。1要素の変更が、周りや子孫の再計算を連鎖的に引き起こすこともある。
- リペイント(repaint):位置は変わらず見た目(色・背景・影など)だけ変わったとき。レイアウトは飛ばしてペイントから。
- 合成のみ:
transform/opacityの変化など。レイアウトもペイントも飛ばせる。最も軽い。
| きっかけ(例) | 発生する処理 | コスト |
|---|---|---|
| width / height / 要素の追加削除 / フォント変更 | リフロー → リペイント → 合成 | 高い |
| color / background / box-shadow | リペイント → 合成 | 中 |
| transform / opacity | 合成のみ | 低い |
JS で「スタイルを書き換える → その直後にレイアウト値を読む」をループで交互にやると、ブラウザは値を正しく返すために毎回その場でリフローを強制されます(強制同期レイアウト)。これが積み重なると一気にカクつきます。対策は、読み取り(offsetWidth 等)をまとめてから、書き込み(スタイル変更)をまとめること。
// ❌ 読み書きが交互 → 毎回リフローを強制(レイアウトスラッシング)
for (const el of items) {
el.style.width = el.offsetWidth + 10 + 'px'; // 書く直前に読む→強制リフロー
}
// ✅ 先に全部“読む”、あとで全部“書く”
const widths = items.map(el => el.offsetWidth); // 読みをまとめる
items.forEach((el, i) => { // 書きをまとめる
el.style.width = widths[i] + 10 + 'px';
});
JavaScript のブロッキングと defer / async
ここが「表示が遅い」の最大の原因になりがちなポイントです。HTML パース中に <script>(特に外部ファイル)に出くわすと、標準ではパースを止めてスクリプトを取得・実行します。なぜなら、スクリプトが document.write などで DOM を書き換えるかもしれないからです。結果、<head> に重い JS を素朴に置くと、その間ずっと画面が真っ白になります。
そこで <script> には2つの属性があります。どちらもダウンロードはパースと並行で行い、パースを止めません。違いは実行のタイミングです。
| 書き方 | ダウンロード | 実行のタイミング | 実行順序 | 向き |
|---|---|---|---|---|
| <script>(無印) | ここで停止して取得 | 取得直後・パースを止めて即実行 | 記述順 | DOM 操作前提の小さなもの |
| <script defer> | 並行で取得 | HTML パース完了後・DOMContentLoaded の直前 | 記述順を保証 | DOM に依存する本体スクリプト |
| <script async> | 並行で取得 | 取得でき次第すぐ(パースを一時中断) | 取得順(バラバラ) | 他に依存しない独立スクリプト |
<!-- 標準:ここでパースが止まる。head に置くと表示が遅れる -->
<script src="app.js"></script>
<!-- defer:並行ダウンロード→パース後に“記述順で”実行。多くの場合の正解 -->
<script src="app.js" defer></script>
<!-- async:取れ次第すぐ実行(順番は保証されない)。計測タグなど独立物向け -->
<script src="analytics.js" async></script>
アプリ本体の JS は defer が無難です。DOM が出来上がった後に、書いた順番どおりに実行されるため、依存関係が壊れにくい。async は「他のスクリプトにも DOM の完成にも依存しない」もの(アクセス解析やA/Bテストの計測タグなど)に向きます。順番が大事な複数ファイルに async を使うと、実行順が崩れて壊れるのが定番の事故です。
<script type="module"> は、書かなくても自動的に defer と同じ挙動になります(パースを止めず、パース後に記述順で実行)。一方で type="module" に async を付けると async 挙動に変わります。「module だから安全」と思い込まず、async の有無で順序保証が変わる点に注意してください。JS 全体の基礎は JavaScript も合わせてどうぞ。
表示を速くするコツ
仕組みが分かると、高速化の打ち手は「各工程を、減らす・遅らせる・避ける」に集約されます。
- CSS は早く小さく:CSS は描画をブロックする。重要な部分だけ先に届け(クリティカル CSS)、残りは後回し。
- JS は遅らせる:本体は
defer、独立物はasync。<head>に素の<script>を置かない。 - リフローを避ける:アニメーションは
transform/opacityで。レイアウト値の読み書きはまとめる。 - 画像にサイズを指定:
width/height(またはaspect-ratio)を入れておくと、画像読み込み後の**ガタつき(レイアウトシフト)**を防げる。 - 配信を速くする:そもそもファイルが速く届けば全工程が前倒しになる。圧縮や CDN、HTTP/2・3 が効く。
async/defer や遅延読み込みは便利ですが、ファーストビューに必要な CSS/JS まで遅らせると、かえって表示が遅く見えます(中身はあるのにスタイルが当たらない一瞬が増える)。「初期表示に要るものは早く、要らないものだけ後回し」の切り分けが肝心です。
まとめ
Web/フロントエンド Article
ブラウザのレンダリングの仕組みを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ブラウザ
比較で見る軸
難易度: intermediate / カテゴリ: Web/フロントエンド / タグ数: 4
導入後に効く点
位置やサイズが変わると レイアウトからやり直す“リフロー”、見た目だけなら“リペイント”。リフローは重いので避け、できれば transform/opacity の合成だけで動かす。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- Web/フロントエンド
- タグ数
- 4
判断チェックリスト
- 自社の用途が「ブラウザ / レンダリング」に近いか確認する。
- 強みである「ブラウザは HTML→DOM、CSS→CSSOM を作り、両者を合わせたレンダーツリーから レイアウト→ペイント→合成 の順で画面を描く(クリティカルレンダリングパス)。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。