ブラウザはページ全体を待って一度だけ描画するわけではありません。HTML をストリーミングし、リソースを発見し、内部 tree を構築し、JavaScript を実行し、geometry を計算し、段階的に paint します。
Critical rendering path は、navigation から初期表示に必要な pixels までの依存チェーンです。network、HTML parser、CSS engine、JavaScript runtime、rendering pipeline を横断します。
有用な性能の問いは「ページはどれくらい大きいか?」だけではありません。どのリソースか main-thread タスクが、次の重要な frame の生成を妨げているか? です。
1. The Rendering Pipeline
簡略化したモデルは次のとおりです:
実際のブラウザはこれらの段階を pipeline 化し、繰り返します。HTML 到着中に DOM を構築でき、リソースは並行ダウンロードでき、document 完了前に paint することもできます。後から JavaScript が style、layout、paint を無効化し得ます。
したがって critical path は固定順序ではなく dependency graph です。遅れた stylesheet は rendering を遅らせます。Parser-blocking script は markup とリソース発見を遅らせます。すべてのリソースが cached でも、大きな JavaScript task は painting を遅らせます。
2. Network、HTML、Resource Discovery
Rendering は document request から始まります。DNS、connection setup、TLS、redirects、server response time はすべて、有用な HTML を parse できる前に起こります。遅い Time to First Byte は後続の段階すべてを後ろにずらします。
ストリーミング HTML により、サーバーがレスポンス生成を終える前にブラウザが処理を始められます。Critical CSS、fonts、images、可視 markup は response の早い段階に置くべきです。
HTML parser は bytes を tokens にデコードし、DOM を段階的に構築します。ブラウザは speculative preload scanner も使い、次のような declarative resources を先読みします:
<link rel="stylesheet"><script src><img src>とsrcset<link rel="preload">
main parser がブロックされていても、この scanner は request を開始できます。JavaScript で作られたリソース、後の API request で返るもの、まだ読み込まれていない CSS 内の参照は、信頼して発見できません。
そのため declarative HTML 自体が性能機能です。<img src> はブラウザに即座に見えます。JavaScript と data fetching の後に計算される image URL は、遅い依存になります。
3. CSS と JavaScript Blocking
CSS は CSSOM に parse されます。適用可能な styles がないと、どの boxes がありどう見えるか決められません。
document head の外部 stylesheet は通常 render-blocking です:
<link rel="stylesheet" href="/app.css" />CSS は通常 DOM construction を止めませんが、未完成 styles に依存する内容の paint は防ぎます。JavaScript が computed styles を参照しうるため、後続 script も遅らせられます:
<link rel="stylesheet" href="/app.css" />
<script>
console.log(getComputedStyle(document.body).color)
</script>Script は CSS を待ち、parser は script を待ちます。一つの stylesheet が rendering と parsing の両方に影響します。
CSS @import は別の waterfall を作ります。import されたファイルは parent stylesheet のダウンロードと parse の後でないと発見されません。直接の <link> 要素の方が request を早く公開します。
Script loading
属性のない classic script は遭遇時に parser をブロックします:
<script src="/legacy.js"></script>代替手段は意味が異なります:
| Script | Execution | Ordering |
|---|---|---|
| Classic | Blocks parser | Document order |
defer | After parsing | Document order |
async | As soon as ready | Load order |
type="module" | Deferred by default | Module dependency order |
アプリケーションコードは通常 module か defer を使うべきです。独立した analytics は async 向きです。
Script を defer しても parser blocking は消えますが CPU コストは消えません。転送、parse、compile、execute は依然必要です。大きな startup tasks は DOM ready 後でも rendering と操作を遅らせます。
4. Style、Layout、Paint、Compositing
Style calculation
ブラウザは DOM と CSSOM を結合し、cascade、inheritance、relative values、matching rules を解決します。結果が rendering に参加する boxes を決めます。
DOM nodes と boxes は一対一ではありません:
display: noneは subtree を layout から除きます。visibility: hiddenは layout 空間を保持します。- Pseudo-elements は DOM nodes なしに boxes を作れます。
- Text は複数の line boxes を生むことがあります。
大きな DOM と広い invalidation は、通常 selectors の微最適化より重要です。
Layout
Layout は box positions、dimensions、line breaks、relationships を計算します。幅の変更は descendants に波及し、テキストを組み直し、container 下のすべてを動かし得ます。
ブラウザは geometry が必要になるまでこの処理を batch します。write と read を交互にすると、繰り返し synchronous layout が強制されます:
for (const card of cards) {
card.style.width = "50%"
console.log(card.offsetWidth)
}write は layout を無効化し、offsetWidth は現在の geometry を要求します。代わりに read を batch してから write します:
const widths = cards.map((card) => card.offsetWidth)
for (const [index, card] of cards.entries()) {
card.style.width = `${widths[index] / 2}px`
}getBoundingClientRect、offsetWidth、getComputedStyle などの API 自体は遅くありません。未完了の style や layout を flush させるときに高コストになります。
Paint and compositing
Paint は text、backgrounds、borders、images、shadows、effects の drawing commands を記録します。ブラウザはこれを tiles に rasterize し、composited layers に載せることもあります。
transform と opacity の animations は、多くの場合 compositor 上で新しい layout や paint なしに動けます:
.panel {
transition:
transform 180ms ease,
opacity 180ms ease;
}width、height、top、left の animation は通常 layout と paint を引き起こします。Compositor 処理も無料ではありません。layers はメモリを使い、rasterization が必要で、毎 frame 組み立てられます。will-change を乱用すると性能が悪化することもあります。
5. Main Thread と Rendering Opportunities
JavaScript と大部分の rendering は main thread を共有します。簡略化した event-loop turn は次のとおりです:
ブラウザは毎 task の後に paint するわけではありませんが、main thread が空く必要があります。長い JavaScript tasks は input と frames を遅らせます。無制限の microtask chain も、microtasks を drain してから進むため rendering を餓死させ得ます。
UI が応答の機会を必要とするとき、CPU-heavy な処理は小さな tasks に分割します。requestAnimationFrame は高コスト計算の場ではなく、視覚的 write を frame に揃えるために使います。通信コストが見合うなら、適した CPU 処理は Web Worker へ移します。
6. 初期ビューの最適化
初期 render には通常、可視 HTML、適用 CSS、blocking scripts、main-thread rendering time、結果に必要な image や font が要ります。Below-the-fold media、analytics、chat widgets、大部分の interaction code は不要です。
依存チェーンは次の順で最適化します:
- Redirects と TTFB を減らし、有用な HTML を早くストリームする。
- Critical CSS、fonts、LCP image を HTML から discover 可能にする。
- Parser-blocking scripts と CSS import chains を除去する。
- 総ページサイズだけでなく critical CSS と JavaScript を減らす。
- Third-party コードを初期 path の外に置く。
- Style、layout、paint、hydration の処理を減らす。
Resource hints は選択的に使います:
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link
rel="preload"
href="/hero.avif"
as="image"
type="image/avif"
fetchpriority="high"
/>preconnect は connection 準備にリソースを使います。preload は bandwidth を奪い合い、最終 request の type と credentials mode と一致させる必要があります。乱用するとより重要なリソースを遅らせます。
Images に dimensions を与え、読み込み前に layout 空間を確保します:
<img
src="/hero.avif"
alt="Browser rendering pipeline"
width="1600"
height="900"
fetchpriority="high"
/>LCP image を lazy-load しないでください。画面外 media には loading="lazy" を使います。
Fonts には WOFF2 subsets を配信し、fallback text を先に描画させます:
@font-face {
font-family: "Inter";
src: url("/fonts/inter-latin.woff2") format("woff2");
font-display: swap;
font-weight: 100 900;
}長いページでは content-visibility: auto が画面外 rendering をスキップできます。CSS containment と list virtualization も処理を減らせますが、挙動が変わるため、計測後に導入すべきです。
7. Framework Rendering と Hydration
Client-rendered application は、JavaScript、framework startup、data fetching が終わるまで意味のある内容を遅らせがちです。
Server rendering は内容とリソース参照を HTML に移し、より早い discovery と paint を可能にします。JavaScript コストは消えません。ページは早く paint しても、大きな hydration task 中に無反応になり得ます。
Streaming SSR、selective hydration、islands、Server Components は同じ問題を狙います。startup 処理を減らし、非 critical JavaScript を初期 dependency chain の外に置くことです。
関連する architecture の問い:
- 最初の HTML response に何を含める必要があるか?
- どの components が client-side JavaScript を要るか?
- Code と data はいつ discover 可能になるか?
- Hydration はより小さな boundaries でできるか?
- Framework が server-to-client request waterfalls を作っていないか?
8. Path の計測
Production build を使い、Network waterfall と Performance trace の両方を確認します:
- Network — 遅い discovery、priority、queueing、transfer time
- Main — parsing、script execution、style、layout、paint、long tasks
- Frames — 逃した rendering deadlines
- Experience — layout shifts と Core Web Vitals
遅い Largest Contentful Paint では、時間を次に分けます:
- TTFB
- Resource load delay
- Resource load duration
- Element render delay
高い load delay は discovery か priority を示します。高い load duration は asset サイズか origin 速度です。高い render delay は CSS、fonts、decoding、JavaScript、main-thread contention を示します。
周辺 metrics は別の失敗を説明します:
- FCP — 最初の内容が現れるタイミング
- LCP — 最大の可視内容が現れるタイミング
- CLS — 予期しない移動
- INP — 次の paint までの interaction latency
LCP が速くても、hydration や third-party scripts が後から main thread を占有すれば INP は悪くなり得ます。Lab traces が原因を説明し、real-user monitoring がユーザーがどれだけ遭遇するかを示します。
最終的なメンタルモデル
Critical rendering path は三つの有限リソースを消費します:
- Network time — critical bytes の発見と転送
- Main-thread time — parse、execute、style、lay out
- Rendering capacity — paint、rasterize、composite frames
速いページは重要リソースを早く公開し、意図しない blockers を避け、startup JavaScript を最小化し、layout 空間を確保し、初期 viewport 外の処理を defer します。
目標はすべてのリソースを即読みすることではありません。navigation とユーザーが見に来た pixels の間で、最短の dependency chain を作ることです。