やあ。今日も今日とて、リポジトリのパフォーマンス改善に頭を悩ませているところかい?
「なぜかウチのプロダクト、低スペックなモバイル端末だとスクロールがカクつくんだよな……」「CSSをいじっただけなのに、なんでこんなに重いんだ?」――フロントエンドの実務現場で生きていると、こういう泥臭い壁にぶ
当たることは日常茶飯事だ。
フレームワークや状態管理の最適化に走るのもいいが、その前に「Webブラウザが裏側でどうやってコードをピクセルに変換しているか」の全体像、つまりクリティカルレンダリングパス(Critical Rendering Path: CRP)の仕組みを心の底から理解しているかい?
ここを抑えていないと、いくらモダンなコードを書いても、ブラウザのメインスレッドを無駄にぶん回す「地雷コード」を量産することになってしまう。今回は、シニアの視点から、このブラウザの心臓部とも言えるレンダリングの裏側を徹底的に紐解いていこう。
—
1. クリティカルレンダリングパス(CRP)の全体像
まず、ブラウザがネットワーク経由でバイト(Byte)単位のデータを受け取ってから、画面に最初のピクセルを描画するまでの旅路を確認しよう。
大まかなステップは以下の6つに分かれる。
1. バイトから文字への変換 (Bytes to Characters): 受信した生データを指定されたエンコーディング(UTF-8など)で文字列に変換する。
2. トークン化 (Tokenization): 文字列をW3CのHTML5標準に基づくトークン(``, `
3. 字句解析・ツリー構築 (Lexing & Tree Construction): トークンをノードオブジェクトに変換し、親子関係を持つDOM(Document Object Model)を組み立てる。
4. CSSOMの構築 (CSS Object Model): CSSも同様のプロセスを経て、カスケードルールを反映したCSSOMを構築する。
5. レンダーツリーの構築 (Render Tree Construction): DOMとCSSOMをマージし、画面に実際に「表示される要素だけ」のツリー構造(Render Tree)を作る。
6. レイアウトとペイント (Layout & Paint): 各ノードの幾何学的位置(サイズや座標)を計算し(レイアウト/リフロー)、ピクセルを画面に描き出す(ペイント/コンポジット)。
この一連のパイプラインをいかにスムーズに流すか、あるいはメインスレッドのブロッキングをどう回避するか。それがフロントエンドエンジニアの腕の見せ所というわけだ。
—
2. レンダリングを狂わせる「魔物たち」:HTML, CSS, JSの相互関係
ここで一つ、現場でよくある誤解を解いておこう。
「DOMの構築とCSSOMの構築は、それぞれ独立して並行処理されている」……そう思っていないかい?
残念ながら、半分正解で半分不正解だ。
CSSは「レンダーブロッキング・リソース」である
ブラウザは上から順にHTMLをパースしていく。そこで `` にぶつかったとする。
ここで重要なのは、CSSOMが完成するまで、ブラウザはレンダーツリーを作れないという点だ。なぜなら、後から読み込まれるCSSによって、すでに構築中のDOM要素のスタイル(色やレイアウト)がガラリと変わる可能性があるからだ。
つまり、CSSの読み込みとパースが完了するまで、ブラウザは画面を描画せず待機する(=レンダリングがブロックされる)。これが、CSSが「レンダーブロッキング」と呼ばれる理由だ。
JavaScriptは「パーサーブロッキング・リソース」である
さらに厄介なのがJavaScriptだ。HTMLの途中で `` が現れると、ブラウザのパーサーは即座にHTMLのパースを中断する。
なぜなら、JSは `document.write` などを使ってDOMの構造を動的に書き換える能力を持っているため、JSの実行が完了するまで次のDOMを構築できないからだ。
さらに、そのJSがCSSOMの構築完了を待つケースもある(JSがスタイルにアクセスする場合など)。
結果として、「HTMLの途中にポンと置かれた素のJSタグ」は、CRPを完全に止める最大のボトルネックになる。
—
3. 実務で使える! CRPを最適化する実践テクニック
理論はこれくらいにして、明日から即座にコードへ反映できる具体的な最適化手法を見ていこう。
① CSSは非同期読み込み(メディアクエリ活用)でブロックを回避する
「すべてのCSSを一つの巨大なファイルにして `
` の先頭で読み込んでいる」というレガシーな構成は今すぐ見直そう。現在表示しているデバイスや状況に関係のないCSSは、メディアクエリを指定してレンダーブロッキングを回避できる。
② JavaScriptには `async` または `defer` を必ず付与する
DOMのパースをブロックさせないために、スクリプトの読み込み属性を適切に使い分けよう。
- `async`: ダウンロードは非同期で行われ、完了次第即座に実行される(HTMLパースは一時中断する)。
- `defer`: ダウンロードは非同期で行われ、HTMLのパースが完全に完了してから上から順番に実行される。DOMの操作を伴うアプリのコードには基本 `defer` を選ぶのが鉄則だ。
—
4. レンダリングの最終工程:レイアウト、リペイント、コンポジットの裏側
無事にレンダーツリーが完成したら、いよいよ画面への出力だ。ここでも無駄なコストを払わないために、3つのフェーズを正しく理解しておこう。
1. レイアウト (Layout / リフロー):
要素の「幅・高さ・位置(座標)」を計算するプロセス。親要素のサイズが変わると、子孫要素すべてのレイアウトが再計算されるため、非常に重い処理だ。
2. ペイント (Paint / リペイント):
レイアウトが決まったら、色、影、境界線などの見た目をピクセル単位に変換して、レイヤーとして描き出すプロセス。
3. コンポジット (Composite / 合成):
ブラウザはページを複数の「レイヤー」に分けて管理している。ペイントされた各レイヤーを合成し、最終的な画面としてGPUに転送して表示する。
シニアが教える「レイアウト・スラッシング(Layout Thrashing)」の回避
JavaScriptでDOMのプロパティを読み取った直後に書き込みを行うと、ブラウザは正しい値を返すために、渋々その場でレイアウトを強制実行させられる。これをループ内で何度も行うと、パフォーマンスが完全に死ぬ。これが「レイアウト・スラッシング」だ。
【悪い例:レイアウト・スラッシングを引き起こすコード】
// ❌ 読み込みと書き込みが交互に行われ、毎回強制レイアウト(リフロー)が発生する
const boxes = document.querySelectorAll(‘.box’);
for (let i = 0; i < boxes.length; i++) {
// offsetWidthの「読み込み」のためにブラウザがレイアウトを強制される
const currentWidth = boxes[i].offsetWidth;
// スタイルの「書き込み」
boxes[i].style.width = (currentWidth + 10) + 'px';
}
【良い例:読み込みと書き込みを完全に分離する】
// 〇 まず「読み込み」をまとめて行う(キャッシュする)
const boxes = document.querySelectorAll(‘.box’);
const widths = Array.from(boxes, box => box.offsetWidth);
// 〇 その後で「書き込み」をまとめて行う
boxes.forEach((box, index) => {
box.style.width = (widths[index] + 10) + ‘px’;
});
このように、ブラウザの描画パイプラインの性質を意識するだけで、無駄なリフローを劇的に減らすことができる。
—
まとめ
Webブラウザのクリティカルレンダリングパスは、いわば「ユーザーが画面を目にするまでの高速道路」だ。
この道路のあちこちに、重いJavaScriptや不要なCSSという名の「工事現場(ブロッカー)」を放置していれば、ユーザーはイライラしてサイトを離脱してしまうだろう。
実務でパフォーマンスチューニングに行き詰まったら、まずはブラウザの開発者ツール(Performanceタブ)を開き、「どこでパースが止まり、どこでレイアウトが走っているのか」を自分の目で確認してみてほしい。
仕組みさえ分かっていれば、フロントエンドの開発はもっとロジカルで、もっと面白くなるはずだ。さあ、今日もクリーンで高速なコードを書きに行こう!

コメント