
ブラウザのレンダリングの仕組み - HTML がピクセルになるまでとリフローの減らし方
当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。
「ページの表示が遅い」「スクロールがカクつく」といった問題を調べていると、リフロー、リペイント、合成、クリティカルレンダリングパスといった言葉に必ず出会います。どれもブラウザの中で動いている一連の処理の一部を指す言葉なので、全体の流れを一度つかんでおくと、原因の見当がつけやすくなります。この記事では、HTMLとCSSが画面のピクセルになるまでの流れを順に追い、リフローとリペイントと合成のみの変更の違い、強制同期レイアウトの直し方、DevToolsでの確かめ方、Core Web Vitalsとの対応までを見ていきます。説明はMDN、web.dev、Chrome公式のドキュメントを出典にしています。エンジン固有の話には「Chromiumでは」のように範囲を書き添えます。
HTMLがピクセルになるまでの全体像
ブラウザの処理は、大まかに次の順番で進みます。
HTML ──(パース)──> DOM ─────┐
├─> スタイル計算 ─> レイアウト ─> ペイント ─> 合成 ─> 画面
CSS ──(パース)──> CSSOM ───┘| 段階 | やること |
|---|---|
| HTMLパース | 受信したHTMLを字句解析し、木構造のDOMを作る |
| CSSパース | CSSのルールを読み、木構造のCSSOMを作る |
| スタイル計算 | DOMとCSSOMを組み合わせ、各要素に効くスタイルを確定する |
| レイアウト | 各要素の大きさと位置を計算する |
| ペイント | 文字、色、罫線、影などの描画内容を作る |
| 合成 | 分割された層(レイヤー)を正しい重なり順で組み合わせ、画面に出す |
MDNの解説によると、ブラウザはHTML全体を待たず、最初のかたまり(通常およそ14KB)が届いた時点でパースを始めます。また、<head>やdisplay: noneの要素は描画の対象から外れますが、visibility: hiddenの要素は場所を取るので対象に残ります。
レイアウトの用語も押さえておきます。MDNでは、最初の計算を「レイアウト」、その後の再計算を「リフロー」と呼び分けています。この記事でもそれに合わせます。
RenderingNGの段階との対応
上の説明はブラウザ共通の大まかなモデルです。Chromiumのレンダリングアーキテクチャ(RenderingNG)では、公式ドキュメントが次の12段階に分けて説明しています。
| RenderingNGの段階 | 内容 | 主に動く場所 | 一般的な呼び方 |
|---|---|---|---|
| Animate | 宣言的なタイムラインに沿って計算済みスタイルを変え、プロパティツリーを更新する | メインまたはコンポジタ | アニメーション |
| Style | DOMにCSSを適用し、計算済みスタイルを作る | メイン | スタイル計算 |
| Layout | 要素の大きさと位置を決め、不変のフラグメントツリーを作る | メイン | レイアウト |
| Pre-paint | プロパティツリーを計算し、既存の表示リストやタイルを必要に応じて無効化する | メイン | (ペイントの準備) |
| Scroll | プロパティツリーを更新してスクロール位置を変える | メインまたはコンポジタ | スクロール |
| Paint | タイルの描き方を記述した表示リストを作る | メイン | ペイント |
| Commit | プロパティツリーと表示リストをコンポジタスレッドへ渡す | メインからコンポジタへの受け渡し | 合成の準備 |
| Layerize | 表示リストを、独立してラスタ化やアニメーションができる合成レイヤーのリストに分ける | コンポジタ | 合成 |
| Raster, decode and paint worklets | 表示リストをGPUタイルに変え、画像をデコードし、ペイントワークレットを動かす | Vizとコンポジタの補助スレッド | ラスタ化 |
| Activate | タイルと視覚効果の描き方を示すコンポジタフレームを作る | コンポジタ | 合成 |
| Aggregate | 見えている各ソースのコンポジタフレームを1枚にまとめる | Viz | 合成 |
| Draw | まとめたフレームをGPUで実行し、ピクセルにする | Viz | 画面への描画 |
「主に動く場所」の列は、公式記事の図と本文から整理したものです。実装は今後も変わりうるので、大まかな目安として読んでください。
注意したいのが「ペイント」という言葉の幅です。MDNはペイントを「各ボックスをピクセルに変える処理」と説明していますが、RenderingNGのPaintは表示リストを作る段階で、実際にタイルへピクセルを焼くのは後のRasterです。資料によって指している範囲が違うので、「ペイントが重い」と言うときは、メインスレッドの表示リスト作成の話なのか、ラスタ化の話なのかを分けて考えると混乱が減ります。
読み込みを止めるものと止めないもの
プリロードスキャナ
DOMの構築はメインスレッドを占有します。そこでブラウザは、メインのパーサーとは別にプリロードスキャナを動かします。MDNによると、プリロードスキャナは手元にある内容を解析して、CSS、JavaScript、Webフォントなどの優先度の高いリソースを裏で要求します。メインのパーサーがそのリソースの参照にたどり着いたときには、すでに取得中か取得済みになっている、というわけです。
スキャナが読むのはHTMLに書かれた参照なので、JavaScriptが実行されてから差し込まれるリソースは早期発見の恩恵を受けられません。LCPに関わるヒーロー画像などは、HTMLに直接書いておくほうが有利です。
CSSはレンダリングを止める
MDNは、CSSの取得を待っている間もHTMLのパースやダウンロードは止まらないものの、JavaScriptは止まると説明しています。スクリプトがCSSの効いた結果を問い合わせることが多いためです。
さらに、CSSが届く前に描画すると、スタイルの当たっていない画面が一瞬見えてしまいます。web.devの解説でも、CSSは既定でレンダリングを止める(レンダリングブロッキングな)リソースとして扱われ、メディアタイプやメディアクエリの指定で一部を止めないものにできる、と説明されています。止めないCSSもダウンロード自体は行われます。これが「クリティカルレンダリングパス」の議論でCSSの量と配信方法が重視される理由です。
同期scriptとasync / defer / type=module
<script>要素はパースに大きく影響します。MDNによると、asyncやdeferのない<script>はレンダリングを止め、HTMLのパースを中断します。プリロードスキャナがこの影響を和らげますが、大きなスクリプトは依然としてボトルネックになります。
| 書き方 | 取得中のHTMLパース | 実行のタイミング | 実行順 |
|---|---|---|---|
<script src="a.js"> | 止まる | 取得後すぐ(パースは止まったまま) | 文書順 |
<script src="a.js" async> | 止まらない | 取得でき次第 | 保証されない |
<script src="a.js" defer> | 止まらない | パース完了後 | 文書順 |
<script src="a.js" type="module"> | 止まらない | 既定でdeferと同様 | 文書順 |
NOTE
実行タイミングはHTML仕様のscript要素の節の記述に基づいています。外部のクラシックスクリプトは、属性がなければ取得と実行が終わるまでパースを止め、deferがあればパースと並行して取得してパース完了後に実行し、asyncがあれば取得でき次第実行します。モジュールスクリプトはasyncがなければパース完了後に実行され、deferを付けても効果はありません。
実際の書き換えは次のようになります。
<!-- 悪い例: 同期 script が HTML のパースを止める -->
<head>
<link rel="stylesheet" href="/css/site-all.css">
<script src="/js/vendor.js"></script>
<script src="/js/app.js"></script>
</head>
<!-- 直した例: 依存順を保ちつつパースを止めない -->
<head>
<link rel="stylesheet" href="/css/site-all.css">
<script src="/js/vendor.js" defer></script>
<script src="/js/app.js" defer></script>
</head>app.jsがvendor.jsに依存している場合、asyncにすると実行順が崩れる可能性があります。依存関係があるスクリプトにはdefer、独立した計測タグなどにはasync、という使い分けが基本です。
リフローとリペイントと合成のみの変更の違い
スタイルを変えたときにパイプラインのどこからやり直しになるかで、処理の重さが変わります。MDNが述べているとおり、リフローはリペイントと再合成を引き起こします。つまり、上流の段階を踏むほど後続の仕事も増えます。
| 種類 | 走る段階 | 代表的なプロパティ |
|---|---|---|
| レイアウト(リフロー)を伴う変更 | スタイル、レイアウト、ペイント、合成 | width、height、margin、padding、top、left、font-size、display |
| ペイントのみの変更(リペイント) | スタイル、ペイント、合成 | color、background-color、box-shadow、visibility |
| 合成のみの変更 | スタイル(または省略)、合成 | transform、opacity |
この表は、仕様やブラウザが公開している対応表を転記したものではなく、各段階の定義(レイアウトは大きさと位置を決める、ペイントは色や影を描く、など)から整理した目安です。実際にどの段階が走るかは、エンジンや要素の状態によって変わるので、気になる箇所はDevToolsで確かめてください。
transformとopacityが軽い理由
RenderingNGの公式ドキュメントによると、Chromiumでは、視覚効果のアニメーションやスクロールはレイアウト、Pre-paint、Paintを飛ばし、メインスレッドを使わずコンポジタスレッドだけで動かせます。transformやopacityの変化は要素の大きさや配置の計算を変えないので、すでにレイヤーになっていれば、合成時の位置や透明度を変えるだけで済みます。
MDNでも、レイヤーを作る例として<video>、<canvas>、opacityや3Dのtransform、will-changeを持つ要素が挙げられています。
/* 悪い例: left を動かすと、フレームごとにレイアウトが必要になる */
.drawer {
position: fixed;
left: -320px;
width: 320px;
transition: left 300ms ease;
}
.drawer.is-open {
left: 0;
}
/* 直した例: 見た目の移動を transform に任せる */
.drawer {
position: fixed;
left: 0;
width: 320px;
transform: translateX(-100%);
transition: transform 300ms ease;
}
.drawer.is-open {
transform: translateX(0);
}スクロールに連動する表現も、JavaScriptでスタイルを書き換える前に、CSSのスクロール駆動アニメーションで書けないか検討する価値があります。画面の切り替え演出ならView Transitions APIという選択肢もあります。
will-changeの注意点
will-changeは、変化する予定のプロパティをブラウザに伝えるものです。レイヤー作成のきっかけになる一方で、MDNはレイヤーについて「メモリ管理の面で高価」なので使いすぎないよう注意しています。実務では次の点を守るのが無難です。
- 全要素に一律で付けず、測定で問題が確認できた要素に絞る
- 常時付けっぱなしにせず、アニメーションの直前に付けて、終わったら外す
- 「とりあえず付ければ速くなる」ものとして扱わない
もう1つ、見た目に影響する副作用があります。will-change: opacityのように、値が変わるとスタッキングコンテキストを作るプロパティを指定すると、実際の変化が起きる前からスタッキングコンテキストが作られるため、z-indexの重なりが変わることがあります。WebKitの変更履歴にも、この挙動に対応した変更が残っています。
フレーム予算とスレッドの役割分担
1フレームに使える時間
画面の更新に遅れないためには、1フレーム分の時間内に処理を終える必要があります。
| リフレッシュレート | 1フレームの時間 |
|---|---|
| 60Hz | 約16.67ms |
| 120Hz | 約8.33ms |
MDNは、滑らかなスクロールとアニメーションのためには、スタイル計算、リフロー、ペイントを含むメインスレッドの処理が16.67ms未満で終わる必要があると説明しています。web.devも、アニメーション中に1フレームを出すための予算を16msとして説明しています。実際には、これにJavaScriptの実行時間も加わり、ブラウザ自身の処理にも時間が要るので、使える時間はさらに短くなると考えておくのが安全です。
メインスレッドとコンポジタスレッド
ChromiumのRenderingNGの説明によると、役割は次のように分かれています。
| スレッド | 主な仕事 |
|---|---|
| メインスレッド | スクリプトの実行、レンダリングのイベントループ、ドキュメントのライフサイクル(Style、Layout、Pre-paint、Paint、Commit)、ヒットテスト、イベントの発火、HTMLとCSSのパース |
| コンポジタスレッド | 入力イベントの処理、スクロールとアニメーションの実行、レイヤー分割の計算、画像デコードやペイントワークレットやラスタ処理の調整 |
公式ドキュメントは、メインとコンポジタのスレッドを分けることが、アニメーションとスクロールをメインスレッドの仕事から隔離するうえで非常に重要だと述べています。これが、JavaScriptが重い場面でもtransformのアニメーションが動き続けられる理由です。
反対に、スクリプトやleftのアニメーションのようにメインスレッドを通るものは、長い処理があるとそのまま止まります。MDNは、重いJavaScriptがメインスレッドを占有すると、スクロールやタッチなどの入力への反応が遅れると説明しています。
強制同期レイアウトとレイアウトスラッシング
ふだんのブラウザは、JavaScriptを実行してから、スタイル計算、レイアウトの順に進みます。スタイルの変更をためておき、フレームの中で1回計算すればよいからです。
ところが、スタイルを変えた直後にレイアウトの結果を読むと、ブラウザは正しい値を返すためにその場で計算せざるを得ません。これが強制同期レイアウト(forced synchronous layout、forced reflow)です。
悪い例と直した例
// 悪い例: クラスを足した直後に offsetHeight を読むので、その場でレイアウトが走る
function logBoxHeight(box) {
box.classList.add('is-large');
console.log(box.offsetHeight);
}
// 直した例: 先に読む。読み取りには前フレームのレイアウト結果を使える
function logBoxHeight(box) {
console.log(box.offsetHeight);
box.classList.add('is-large');
}強制同期レイアウトが1回なら影響は限定的でも、ループの中で繰り返すと深刻になります。読み取りと書き込みが交互に続き、毎回スタイル計算とレイアウトが走る状態をレイアウトスラッシングと呼びます。
const box = document.querySelector('.box');
const paragraphs = document.querySelectorAll('.box p');
// 悪い例: 反復ごとに「読む、書く」が交互に起き、毎回レイアウトをやり直す
function matchWidth() {
for (const p of paragraphs) {
p.style.width = `${box.offsetWidth}px`;
}
}
// 直した例: 読み取りは先に1回だけ、書き込みはその後にまとめる
function matchWidth() {
const width = box.offsetWidth;
for (const p of paragraphs) {
p.style.width = `${width}px`;
}
}要素ごとに違う値を読む場合も、考え方は同じです。読み取りのループと書き込みのループを分けます。
function alignCards(cards) {
// 読み取りだけのループ
const heights = Array.from(cards, (card) => card.offsetHeight);
// 書き込みだけのループ
cards.forEach((card, index) => {
card.style.minHeight = `${heights[index]}px`;
});
}offsetHeightやoffsetWidth以外にも、レイアウトを強制するプロパティは多くあります。「読み取りは先にまとめる」という原則で書いておけば、個々のプロパティを覚えていなくても避けられます。
requestAnimationFrameの位置づけ
web.devの解説では、requestAnimationFrameでスケジュールした関数は次のフレームの先頭で実行され、その時点では前のフレームのレイアウト結果を読めます。そのため、レイアウトの値を読みたい処理の置き場所として適しています。
ただし、requestAnimationFrameで包めばレイアウトスラッシングが直るわけではありません。コールバックの中で書いてから読めば、やはり強制同期レイアウトが起きます。
// 悪い例: rAF の中でも読み書きが交互なら強制レイアウトが発生する
requestAnimationFrame(() => {
for (const p of paragraphs) {
p.style.width = `${box.offsetWidth}px`;
}
});正しい使い方は「フレームの先頭で読み、その後で書く」です。resizeのように短い間隔で何度も発火するイベントでは、1フレームに1回だけ処理するようにまとめます。
let scheduled = false;
function update() {
const width = box.offsetWidth; // 先に読む
for (const p of paragraphs) {
p.style.width = `${width}px`; // 後でまとめて書く
}
}
window.addEventListener('resize', () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
update();
});
});イベントの間引き方そのものはdebounceとthrottleの解説で扱っています。
HTML仕様のイベントループとの関係
ブラウザのイベントループは「タスクを1つ実行し、マイクロタスクを空にし、必要なら画面を更新する」というサイクルを繰り返します。流れの詳細はJavaScriptの非同期とイベントループで解説しています。この「画面を更新する」部分が、HTML仕様のイベントループの処理モデル(8.1.7 Event loopsの節)にあるupdate the renderingです。
この関係から、次のことが分かります。
- タスクの実行中は画面が更新されないので、長いタスクはそのままフレームの遅れになる
- 通常のスタイル変更はためられ、次の描画更新でまとめて処理される
- 強制同期レイアウトは、タスクの途中で読み取りによってこの計算を前倒しさせる
NOTE
2026年11月時点のHTML仕様では、update the renderingの手順は次の順に並んでいます。resizeのステップ、scrollのステップ、メディアクエリの評価、アニメーションの更新とイベントの送出、fullscreenのステップ、requestAnimationFrameのコールバック、そのあとにスタイルの再計算とレイアウトの更新です。つまりrequestAnimationFrameの中で行ったDOMの変更は、同じ更新の中のスタイルとレイアウトの計算に反映されます。
レンダリングの負荷を減らす手段
content-visibility
content-visibility: autoを指定すると、画面外にある要素の描画処理を省けます。長い一覧や記事ページで特に効果を期待できるプロパティです。MDNではBaseline 2024の機能とされ、2024年9月以降は主要ブラウザの最新版で利用できます。
.article-list__item {
content-visibility: auto;
contain-intrinsic-size: auto 480px;
}処理を省かれた要素は高さが分からず、そのままだと0に潰れてしまいます。そこでcontain-intrinsic-sizeで仮の大きさを与えます。auto 480pxの形で書くと、一度描画された要素は最後の描画サイズを使い、まだ描画されていない場合は指定した長さを使います。この指定を忘れると、スクロールバーの長さが変わったり、スクロール時に要素が動いて見えたりします。
CSS containment
web.devは、レイアウトの範囲は通常ドキュメント全体に及ぶと説明しています。要素が多いほど、また一般にDOMが大きいほど、レイアウトのコストは上がります。
CSSのcontainプロパティは、ある要素の内側の変更が外側に影響しないことをブラウザに宣言し、処理の範囲を限定するためのものです。指定できる値の一覧と定義はCSS Containmentの仕様で確認してください。値ごとの挙動はこの記事では扱いません。
そのほかの基本
- DOMのノード数を増やしすぎない。MDNも、ノードが多いほどDOM構築に時間がかかると説明しています
<img>にはwidthとheightを指定する。MDNの例のとおり、サイズの分からない画像は読み込み後にリフローを起こします- アニメーションは可能なら
transformとopacityで表現する - 重い処理は短いタスクに分割し、メインスレッドを長時間占有しない
DevToolsのPerformanceパネルで確認する
推測で直す前に、どの段階に時間がかかっているかを確かめます。Chrome DevToolsでの基本的な流れは次のとおりです。
- Performanceパネルで記録を開始し、遅いと感じる操作やスクロールを行って記録を止める
- メインスレッドのトラックで、長いタスクの中身を確認する
- スタイル計算(Recalculate Style)、レイアウト、ペイントのどれが長いかを見る
- Insightsサイドバーで、ブラウザが検出した問題を確認する
MDNによると、CSSのパース、CSSOMの構築、スタイルの計算は、DevToolsではRecalculate Styleにまとめて計上されます。
Chrome公式のドキュメントでは、次のようなインサイトが説明されています。
| インサイト | 分かること | 関連する問題 |
|---|---|---|
| Forced reflow | JavaScriptがレイアウトの再計算を強制している箇所と、その呼び出し履歴 | 強制同期レイアウト |
| Layout shift culprits | 予期しない移動を起こしたDOM要素 | CLS |
| INP breakdown | INPのサブパートごとの内訳 | INP |
項目名は執筆時点の公式ドキュメントに合わせたものです。Chromeの更新で表記が変わることがあります。
実際のユーザー環境で調べたい場合は、Long Animation Frame APIも使えます。web.devによると、スクリプトの帰属情報にあるforcedStyleAndLayoutDurationで、強制的なスタイル計算とレイアウトに費やした時間を確認できます。
Core Web Vitalsとの対応
レンダリングの知識は、Core Web Vitalsの改善に直結します。web.devが示す「良好」の基準と、関連する処理を並べます。web.devは、モバイルとデスクトップに分けて、ページ読み込みの75パーセンタイルで評価することを推奨しています。
| 指標 | 良好の基準 | 関わる処理 | 主な打ち手 |
|---|---|---|---|
| LCP | 読み込み開始から2.5秒以内 | HTMLパース、CSSとscriptの読み込み、最初のペイント | 同期scriptを減らす、CSSを軽くする、LCP要素をHTMLに直接書く |
| INP | 200ミリ秒以下 | 入力後のJavaScript実行、スタイル計算、レイアウト、ペイント | 長いタスクの分割、強制同期レイアウトの解消 |
| CLS | 0.1以下 | 読み込み後のリフローによる要素の移動 | 画像や埋め込みに寸法を指定する、contain-intrinsic-sizeを適切に設定する |
対応づけの要点は、次のように整理できます。
- LCPが悪いときは、読み込みとブロッキングの話(プリロードスキャナ、CSS、script)から見る
- INPが悪いときは、メインスレッドの話(長いタスク、強制同期レイアウト、フレーム予算)から見る
- CLSが悪いときは、レイアウトの話(寸法の指定漏れ、後から挿入される要素)から見る
まとめ
- ブラウザはHTMLとCSSからDOMとCSSOMを作り、スタイル計算、レイアウト、ペイント、合成を経て画面を作る
- ChromiumのRenderingNGでは12段階に分かれており、PaintとRasterは別の段階になっている
- 属性なしの
<script>はパースとレンダリングを止める。依存関係があるならdefer、独立したものならasync - リフローはリペイントと再合成を伴う。上流の段階を踏む変更ほど重い
- Chromiumでは、視覚効果のアニメーションやスクロールはコンポジタスレッドだけで動かせる
will-changeはレイヤーを増やしメモリを使い、スタッキングコンテキストにも影響するので、測定して必要な要素に絞る- 強制同期レイアウトは「書いてから読む」順序で起きる。読み取りを先にまとめて、書き込みを後にまとめる
requestAnimationFrameは読み書きの順序まで直してくれるわけではない- 60Hzのフレーム予算は約16.67msで、ブラウザ自身の処理の分を考えるとさらに短い
- どの段階が重いかはDevToolsのPerformanceパネルで確かめ、LCP・INP・CLSのどれに効く問題なのかを切り分ける
処理の順番が頭に入っていると、「これはレイアウトまで戻る変更か」「メインスレッドを通る処理か」という視点でコードを読めるようになります。まずは手元の遅いページを1つ記録して、どの段階に時間がかかっているかを見るところから始めてみてください。
参考
- Populating the page: how browsers work(MDN)
- RenderingNG architecture(Chrome for Developers)
- Avoid large, complex layouts and layout thrashing(web.dev)
- Web Vitals(web.dev)
- The CSS content-visibility property is now Baseline Newly available(web.dev)
- content-visibility(MDN)
- Performance Insights(Chrome for Developers)
- Insights sidebar in the DevTools Performance panel(Chrome for Developers)
- HTML Standard: Web application APIs(WHATWG)
- Changeset 188514(WebKit)


