ブラウザのレンダリングの仕組み - HTML がピクセルになるまでとリフローの減らし方

ブラウザのレンダリングの仕組み - HTML がピクセルになるまでとリフローの減らし方

作成日:
読了:約26分
更新日:

当サイトは 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宣言的なタイムラインに沿って計算済みスタイルを変え、プロパティツリーを更新するメインまたはコンポジタアニメーション
StyleDOMに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での基本的な流れは次のとおりです。

  1. Performanceパネルで記録を開始し、遅いと感じる操作やスクロールを行って記録を止める
  2. メインスレッドのトラックで、長いタスクの中身を確認する
  3. スタイル計算(Recalculate Style)、レイアウト、ペイントのどれが長いかを見る
  4. Insightsサイドバーで、ブラウザが検出した問題を確認する

MDNによると、CSSのパース、CSSOMの構築、スタイルの計算は、DevToolsではRecalculate Styleにまとめて計上されます。

Chrome公式のドキュメントでは、次のようなインサイトが説明されています。

インサイト分かること関連する問題
Forced reflowJavaScriptがレイアウトの再計算を強制している箇所と、その呼び出し履歴強制同期レイアウト
Layout shift culprits予期しない移動を起こしたDOM要素CLS
INP breakdownINPのサブパートごとの内訳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に直接書く
INP200ミリ秒以下入力後のJavaScript実行、スタイル計算、レイアウト、ペイント長いタスクの分割、強制同期レイアウトの解消
CLS0.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つ記録して、どの段階に時間がかかっているかを見るところから始めてみてください。

参考

Baseline 入門 - 「この機能もう使っていい?」をチームの共通語にする

Baseline 入門 - 「この機能もう使っていい?」をチームの共通語にする

約8分

Web の機能を採用するときの判断軸 Baseline を整理します。Newly available と Widely available の2段階、コアブラウザ集合の定義、Baseline 年バッジ、MDN / Can I Use / web.dev での表示、そして Browserslist や ESLint への組み込みまで。「caniuse を毎回見る」から「Baseline で判断する」へ、チームの共通言語にする方法をまとめます。

CSS 2026 - ブラウザに仕事をさせる新機能まとめ(contrast-color・shape・Anchor Positioning ほか)

CSS 2026 - ブラウザに仕事をさせる新機能まとめ(contrast-color・shape・Anchor Positioning ほか)

約9分

2026年にBaseline入りした、あるいは主要ブラウザが出揃ったCSSの新機能を整理します。contrast-color()、shape()、:active-view-transition、Anchor Positioning、@scope など、これまでJSやライブラリでやっていたことをブラウザ標準だけで実現する機能を、用途と注意点つきでまとめます。

楽天市場のおすすめPR / 楽天アフィリエイト