ブラウザのレンダリングの仕組み
投稿日: 2025年07月10日
ShiftBのカリキュラムで、Reactの勉強しているとレンダリングという言葉を聞くと思います。 コンポーネントがHTML要素として画面に表示される過程のこと。
例えば、データや状態が更新されると、自動的に変更部分が再レンダリングされ、画面に反映される仕組みです!
このように普段便利なライブラリを使っているけど、じゃあブラウザ側でのレンダリングの仕組みってどうなっているの? ブラウザの仕組みを知ることで、パフォーマンス向上の勉強になりそう!っと思って、 今回、ブラウザのレンダリングの仕組みについて書こうと思います。
内容が長めなので、読むのめんどくさいです笑
ここでは、どのように画面に表示されて便利なライブラリやアニメーションが表示されるようになっているのかをパフォーマンスとフレームレートの関係について見ていきます!
例えば、アニメーションとか使ってUIを作って画面に表示させる時、パフォーマンスが悪く動きがカクカクしたり、スクロールがカタついたりするのはなぜでしょう?
これはフレームレートの低下が原因で起こります。
フレームレートはfps(frames per second)という単位で表します。
この数字が低ければ低いほど、動きが悪くカタつくってわけです。
例えるなら、パラパラ漫画を想像してください。 枚数が少ないと動きはカタつくが、枚数が多ければ多いほど動きが滑らかになりますよね? なので、これと同じで、フレームレートが低下するというのは、「フレーム数が減る=パラパラ漫画のページ数が少なくなる」状態なので、動きがカクついて見えるということなのです。
ブラウザの場合60fpsが最高となり、パフォーマンス的に良い状態と言われています。 これは、一秒間に60回画面が更新されていることになります。
スクロールする動きやアニメーションの動きは、静止画面を高速で切り替えることで、 動いているように表示させている仕組みです。 ※高リフレッシュレート環境では、それ以上のfpsになることもある。
60fpsということは、
1秒を60で割ると…
→ 1回の更新にかかる時間は約16.6ミリ秒(= 1000ミリ秒 ÷ 60)
60fps = 約16.6msごとに1回画面が切り替わる
→ この速さで画面が更新されているから、滑らかに見える!
しかし、JavaScriptやレンダリングに重い処理があると、画面更新の処理までに間に合わず、 フレームレートを低下させてしまいます。 これがカタつき、劣化につながるのです。
つまり、ブラウザで画面のパフォーマンスが悪いとなった場合、16ms以内に処理を完了していないってことになるので、重い処理がないか、頻繁に呼ばれている処理がないかなどを見ていくことになります。
JavaScriptを改善する場合は自分の書いたコードの負荷の重いところをDevTools (開発者ツール) で見つけて改善する方法もありますが、レンダリングはブラウザ側の処理ではブラックボックス化(中身が見えない)ため、基礎知識がないと修正するのは困難かもしれないです。 こういったところから、レンダリングの知識は問題解決として有効になります。
下の図は、ブラウザが各フレームごとに行なっている処理となります。(レンダリングの代表的な処理でもあります) この処理は、ブラウザのコアな部分のメインスレッドです。
スレッドとは、プログラムが起動してから処理を実行し終えるまでのことです。
ブラウザでは、Parse(パース) > Style(スタイル) > Layout(レイアウト) > Paint(ペイント) > Composite(コンポジット)の流れで、画面にどのように表示させるのかを計算しています。
例えば、画面表示の際にはブラウザはこの処理フローを最初から行います。
逆に画面表示内容に更新があった場合は、必要な工程のみ処理し、再計算が不要な工程は省略します。 ということは、どのフロー関連しているのかを知っているだけで、ブラウザの負担を大きく減らすことができます。
それでは、それぞれの関係を見ていきましょう!
Parseでは、HTMLとCSSの構文解析が行われます。
ブラウザはHTMLとCSSを平行して構文解析し、 DOM Tree(ドムツリー)とStyle Rules(スタイルルールズ)という処理しやすい構造体に変換します。
つまり、これらの構造体を生成するのがParseの役割です。
表示する対象のコンテンツを解析する処理をする部分です。
HTMLをパース(解析)して、DOMツリーを作成します。 DOMツリーの一番上は、documentになります。
CSSをパースして(解析)、HTMLと同じようにツリー構造にして、Style Rulesを作成します。
ドキュメントオブジェクトモデル (Document Object Model, DOM) は、ウェブページを表す HTML のような文書の構造をメモリー内に表現することで、ウェブページとスクリプトやプログラミング言語を接続するものです。
ブラウザのレンダリングを担当する機能をレンダリングエンジンと呼びます。 SafariはWebkit、ChromeはBlink、FirefoxはGecko、EdgeはEdgeHTMLというそれぞれのレンダリングエンジンが使用されています。 それぞれブラウザのレンダリング処理フローが異なるため、ブラウザによる挙動の違いがあります。
続いては、Styleの工程です。
この工程では、先ほど作成してDOM TreeとStyle Rulesの紐付けが行われます。
具体的には、どのスタイルがどの要素に適用されるかをマッチングし、複数個のスタイルが 一致する要素に関しては、スタイル適用の優先順位に従って最終的に適用されるスタイルを 割り出します。
この作成したものを次のLayoutで描画させます。
ここでできた、スタイルと要素をマッチングはRender Tree(レンダーツリー)と呼ばれます。 Render Treeの各要素をRenderer(レンダラー)と呼びます。
先ほど作成された Render Tree(レンダーツリー) によって、
「どの要素にどのスタイルが適用されているか」がブラウザに伝わります。
この次の工程では、各要素の 位置と大きさを計算 します。
この処理は Layout(レイアウト) または Reflow(リフロー) と呼ばれます。
レンダーツリーの 上流から下流(親→子)に向かって 再帰的に計算されます。
初回のページ表示時には、最上位の <html>
タグから順に、階層ごとにすべての要素を処理していきます。
画面が更新される場合(例:JavaScriptで要素を追加・変更したとき)、
ブラウザは できるだけ計算範囲を最小限に抑える ようにします。
そのため再計算が必要になるのは、次のような範囲に限定されます:
変更があった要素自身
その子要素
同じ階層の兄弟要素
Layoutの工程では、最終的に Layout Tree(レイアウトツリー)が作成されます。
Layout Tree とは、ブラウザが「何をどこに表示するか」を計算するために生成する構造で、画面に実際に表示される要素のみが含まれます。
たとえば、display: none
が指定された要素は完全に無視され、Layout Tree には含まれません(DOMには存在するが、レイアウト処理には登場しません)。
一方で、visibility: hidden
のように画面上は見えないがスペースが確保される要素は
Layout Tree に含まれます。
さらに、::before
や ::after
などの擬似要素も、視覚的に表示されるためレイアウト対象として扱われます。
つまり、Layout Tree には「画面に描画される要素」だけでなく、「レイアウトに影響を与える要素」も含まれるのが特徴です。
Layout の計算には、大きく以下の2種類があります。
Global Layout(グローバルレイアウト)
ページ全体に関わる変更(例:ブラウザの幅変更、フォントサイズの変更など)により、全体のレイアウトが再計算されます。そのため負荷が大きめです。
Incremental Layout(インクリメンタルレイアウト)
一部の要素に限定された変更(例:要素の追加や削除)に対して実行される、軽量なレイアウト計算です。Global Layoutと比べ、軽い処理になります。
この Layout の再計算は、JavaScript によって要素の位置やサイズが変更されたとき、またはレイアウトに関係する以下のような スタイルプロパティが変更されたとき に発生します。
height
, width
, padding
, margin
, top
, right
, left
, bottom
, box-shadow
など。
Parse > Style > Layout を経て、要素を出力する位置や大きさ、色が変わります。
Paintは、簡単に言うとabsoluteやfixedといった、要素と要素が重なる時にどっちを上にするのかを加味しながら画面を生成する役割です。
Paintの工程では要素の重なりを考慮して、順番に命令を作成することで正しく描画できるようにします。 この命令をPaint Records(ペイントレコーズ)と言います。
Pain Recordsの命令の順番は、「Stacking Contexts(スタッキングコンテキスト)」をもとに決定されます。 スタッキングコンテキストでは、要素の出力順を保持します。
どっちの要素を先に描写させるのかを命令する役割です。
下の図を見てください。 右側にある、ペイントレコーズによって要素の重なり順によって生成されます。 背面に来る要素を先に命令に出して、どんどん上書きしていけば全面に来るものが画面上に来る仕組みとなってます。
背面要素 → 先にペイント
前面要素 → 後からペイント(上書きされる)
DOM順や z-index
に基づいて ペイント順序 が決まる
Paint Records はこの描画指示を保持する
絵を描くときに「背景」→「人物」→「文字」みたいに、下から順に重ねて描いていくのと同じです。
モダンブラウザでは同時にLayer Tree(レイヤーツリー)を作成します。
Layerの分離は変更の際の計算量を少なくするためにとても有効です。
例えば、Layerを分離せず描写を行った場合、変更があった際に他の要素との変更を考慮して多くの計算を行う必要があります。
しかし、レイヤーを分離して他の要素を考慮しないでよい状態にしておくと、そのレイヤーのみ再計算を行えば良いことになり、計算量を大幅に削減できるようになります。
つまり、レイヤーを分割することによってパフォーマンス向上になるってことです。
Layer Treeのレイヤーを分離する条件に先ほどのz-indexは、関係ないです。
3Dトランスフォームtransform: translateZ(0)やtransformを使用したアニメーションを検知した際にブラウザはレイヤーを分離します。
また、will-changeを使用した場合にもLayerは分けられます。
ここまでの処理がブラウザのMain Thread(メインスレッド)上で行われます。最終的に作成されたLayer TreeやPain Recordsは、Compositor Thread(コンポジタースレッド)に渡され、Main Threadは開放されます。
Webページの描画の最後のステップが「Composite(合成)」です。
まず、Main Thread(メインスレッド)が「何をどう描くか」の命令(Paint Records)を作ります。
その命令をもとに、Compositor Thread が「どの部分を再描画するか」を判断し、レイヤー単位で描画の準備をします。
続いて、Raster Thread が各レイヤーをピクセル単位で画像化します(これをラスタライズと言います)。この処理は複数スレッドで並列に行われます。
最後に、GPU がすべてのレイヤー画像を重ねて、1枚の画面に合成します。これがユーザーに表示されるWebページです。
アニメーションによく利用されるtransform、opacityの適用もこの工程で行われます。そのためtransform, opacityの変更はレイアウトプロパティー(left,topなど)の変更に比べて、軽い処理となります。
transform, opacity
例えば、Layoutの部分で、物体の位置を変えたとき、アニメーションで変更したするとLayoutの工程に作用されます。
つまり、後続のPaintやCompositeの処理も再計算されるためパフォーマンスが悪くなる。 アニメーションを使うなら、Compositeの工程で使うことがおすすめ。
Compositor Thread(コンポジタースレッド):
メインスレッドの代わりに描画を担当
Raster Thread(ラスタースレッド):
レイヤーごとの画像を**ピクセル単位で塗る(ラスタライズ)**担当
Paint Records をもとにレイヤーを合成
レイヤーごとにピクセルデータを生成(=ラスタライズ)
最後にすべてのレイヤーを重ねて1枚の画面を作成(合成)
ページ全体にわたるような大きなレイヤーもある
計算負荷が高いため、1スレッドでは間に合わない
下図のように、レンダリングの工程によってブラウザは異なるスレッドを使用します。
レンダリングは、ほとんどがMain Treadで行われます。
Compositeから、Compositor ThreadやRaster Thread 4スレッドほどあります。(ブラウザによる)
最終的にGPUに指示が送られる。
Webページを表示するうえでの「司令塔」。
HTMLの解析、CSSの適用、レイアウト、描画命令の生成(Paint)など、基本的な処理をすべて担当します。
JavaScriptの実行もこのスレッドで行われるため、負担が大きく、なるべく仕事を減らすことが重要です。
画面のレイヤーをまとめて1枚に合成する担当。
自分でピクセルを塗るのではなく、Raster Threadに「塗って」と依頼します。
塗り終わったレイヤーを集めて、重ね順に合成して画面を作ります。
なお、アニメーションでレイアウトが変わるときなどは、Main Threadが一時的に合成処理を担当することもあります。
レイヤーを画像に変える担当(これを「ラスタライズ」と言います)。
複数(通常4つ)のスレッドが並列で動き、Compositor Threadから来た塗りの依頼を高速で処理します。
最終的に画面に表示する役割を担います。
特に transform
や opacity
を使ったアニメーションでは、レイヤーはすでにできていて、Main Threadを使わずにGPUだけで描画できるので、高速で滑らかに動きます。
ブラウザはこのようにスレッドを分けることで画面表示を最適化しています。
HTMLやCSSの変更は、ブラウザ内で「Style → Layout → Paint → Composite」というステップを踏んで画面を描画します。
JavaScriptでスタイルを変更するとき、どのステップに影響するかによって処理の重さが変わるのがポイントです。
Style → Layout → Paint → Composite の全工程が再実行されます。
位置やサイズを再計算する Layout
に関わるため、特に処理が重くなります。
子要素が多いとその分、再計算も増えるので注意!
height, width, margin, padding, top, left
などの変更はLayoutに影響。
Layoutは Main Thread(メインスレッド)で動くため、JavaScriptの他の処理も詰まりやすい。
アニメーションには使わない方が良い。代わりに transform
を使う。
もし使うなら、will-change
でパフォーマンスを最適化する。
Layoutはスキップされ、Paint → Composite が実行されます。
Layoutよりは軽いですが、Main Thread上で動くので、負荷が高い場合もありえます。
ある要素の色を変更した場合、Layout(位置や高さ)は関係ないのでスキップされます。
Paint、Compositeが呼ばれます。
Layoutの処理がない分、先ほどより処理時間は短くなりますが、 Main Threadは変わらず使用されるので、処理が重くなるとこがあります。
そのため、処理が軽く済むopacityを代用するのがおすすめ!
background-color, background-image, border-color, colorなど
background-color, border-color, color
などはPaintに関係。
可能なら opacity
で代用するのがおすすめ(より軽い処理で済む)。
どうしてもPaint処理が必要なら will-change
を使って負荷を減らす工夫を。
transform
や opacity
の変更は、Composite のみが関わる。
この工程は Compositeスレッドで処理されるため、Main Threadに影響を与えません。
その結果、アニメーションが 非常に滑らかになります。
transform, opacity
transform, opacity
はパフォーマンスに優しい。
アニメーションや動的な変化には transform を優先的に使うべき。
ただし、ブラウザによっては違いがある(例:Safariでは transform
でもLayoutが再計算されることがある)。
以下のリンクで、どのプロパティーがどの工程に作用されているのかが確認できます。
興味がある人は、覗いてみてください。
Chrome Devtools による フロントエンドパフォーマンスの計測に参考となる記事となります。
ここで紹介したものは、実際にレンダリングエンジンによって異なるので絶対ではないですが、 基本的な仕組みを理解するとレンダリングに負担をかけずに実装が行えます。
トラブルが起こった際には、見るべきところがわかってくるので、解決につながる材料になると思います。