Browser 不會等整頁下載完才一次渲染。它會串流 HTML、發現資源、建立內部 trees、執行 JavaScript、計算 geometry,並逐步 paint。
Critical rendering path 是從 navigation 到初始畫面所需 pixels 之間的依賴鏈。有用的問題是:哪一項資源或 main-thread 任務,正阻止 browser 產出下一個重要的 frame?
1. The Rendering Pipeline
簡化 pipeline 是 HTML → DOM,CSS → CSSOM,然後 style、layout、paint、compositing、pixels。真實的 browser 會 pipeline 並重複這些階段。
HTML → DOM ─┐
├→ Style → Layout → Paint → Compositing → Pixels
CSS → CSSOM ┘Navigation → HTML bytes arrive
├─ HTML parser builds DOM
│ ├─ CSSOM ready?
│ │ No → Cannot paint safely
│ │ Yes → Style → Layout → Paint
│ └─ Parser-blocking script?
│ Yes → Pause parsing → Download + execute JS → back to parser
│ No → Continue parsing
└─ Preload scanner discovers resources → Download CSS / JS / images
└─ CSS download also feeds CSSOM ready- DOM 可以在 HTML 仍在到達時建立。Browser 可以在 document 尚未完成時就 paint。之後 JavaScript 仍可能讓 style、layout 或 paint 失效。
- 這條 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,都發生在 browser 能解析有用 HTML 之前。緩慢的 Time to First Byte 會把後面每個階段都往後推。
- 串流 HTML 讓 browser 能在 server 尚未完成前就開始處理回應。Critical CSS、fonts、images,以及可見的 markup,應盡早出現。
- HTML parser 逐步建立 DOM。Speculative preload scanner 會預先尋找
<link rel="stylesheet">、<script src>、<img src>/srcset,以及<link rel="preload">。 - Scanner 可以在 main parser 被擋住時仍開始發 request。它無法可靠地發現由 JavaScript 建立、由稍後 API 回傳,或引用於尚未載入 CSS 裡的資源。
- Failure: 在 JavaScript 與資料抓取之後才算出來的 image URL。HTML 裡的
<img src>對 browser 立刻可見。Declarative HTML 本身就是一項效能特性。
3. CSS 與 JavaScript Blocking
CSS 會被解析成 CSSOM。Browser 需要適用的 styles,才能決定有哪些 boxes,以及它們應如何呈現。放在 document head 的外部 stylesheet,通常是 render-blocking。
<link rel="stylesheet" href="/app.css" />
<script>
console.log(getComputedStyle(document.body).color)
</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 |
Classic script:
Parser → Network: Discover /legacy.js
Parser: Pause HTML parsing
Network → Main thread: Script ready
Main thread: Execute script
Main thread → Parser: Resume parsing
defer / module:
Parser → Network: Discover /app.js
Parser: Keep parsing HTML
Network → Main thread: Script ready
Parser → Main thread: DOM complete
Main thread: Execute deferred scripts- CSS 通常不會阻止 DOM construction,但會阻止依賴未完成 styles 的內容被 paint。它也可能延遲後面的 script,因為 JavaScript 可能會檢查 computed styles——script 要等 CSS,parser 又要等 script。
- CSS
@import會造成 waterfall:imported files 要等到 parent stylesheet 解析後才會被發現。直接的<link>能更早暴露 requests。 - 應用程式碼通常應使用 module 或
defer。獨立的 analytics 可以用async。 - Failure: 把
defer當成免費。它移除的是 parser blocking,不是 CPU 成本。Transfer、parse、compile 與 execute 仍然會發生。大型 startup task 即使在 DOM ready 之後,仍可能延遲 rendering。
4. Style、Layout、Paint 與 Compositing
Style 解析 cascade。Layout 計算 geometry。Paint 記錄 drawing commands。Compositing 把 layers 組裝成 frames。
for (const card of cards) {
card.style.width = "50%"
console.log(card.offsetWidth)
}Property change → Which stage?
width / height / top / left → Layout → Paint → Composite
color / background / box-shadow → Paint → Composite
transform / opacity → Composite only- DOM nodes 與 boxes 並非一對一對應:
display: none會把 subtree 從 layout 移除;visibility: hidden會保留空間;pseudo-elements 可以在沒有 DOM nodes 的情況下建立 boxes。 - 大型 DOMs 與大範圍 invalidation,通常比微優化普通 selectors 更重要。寬度變更可能重排文字,並移動 container 下方的所有內容。
- 動畫
transform與opacity常常可以在 compositor 上跑,不必新的 layout 或 paint。動畫width、height、top或left通常會觸發 layout 與 paint。 - Failure: 交錯 writes 與 reads——先設
style.width,再讀offsetWidth——會在 loop 裡強制 synchronous layout。先 batch reads,再 writes。getBoundingClientRect本身並不慢;它昂貴是因為它會 flush pending work。到處用will-change可能讓效能更差:layers 占用 memory。
5. Main Thread 與 Rendering Opportunities
JavaScript 與大部分 rendering 工作共用 main thread。Browser 不會在每個 task 之後都 paint,但它需要 main thread 變得可用。
Run one task → Drain microtasks → Update rendering?
Yes → requestAnimationFrame → Style → Layout → Paint → Composite → Next task
No → Next task- 長 JavaScript tasks 會延遲 input 與 frames。
- 用
requestAnimationFrame把視覺 writes 對齊到 frame,而不是拿它來做昂貴計算。當通訊成本說得通時,把合適的 CPU 工作移到 Web Worker。 - Failure: 無界的 microtask 鏈會餓死 rendering,因為 checkpoint 會先排空,browser 才來得及 update the rendering。
6. 優化初始畫面
初始 render 需要可見 HTML、適用的 CSS、blocking scripts、main-thread rendering time,以及結果所需的任何 image 或 font。它不需要 below-the-fold media、analytics、chat widgets,或大多數 interaction code。
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link
rel="preload"
href="/hero.avif"
as="image"
type="image/avif"
fetchpriority="high"
/>
<img
src="/hero.avif"
alt="Browser rendering pipeline"
width="1600"
height="900"
fetchpriority="high"
/>- 減少 redirects 與 TTFB;盡早串流有用的 HTML。讓 critical CSS、fonts 與 LCP image 能從 HTML 被發現。
- 移除 parser-blocking scripts 與 CSS import chains。減少 critical CSS 與 JavaScript,而不只是總頁面大小。把 third-party code 留在初始 path 之外。
- 給 images 尺寸,讓 layout 空間在它們載入前就存在。不要 lazy-load LCP image。對 offscreen media 用
loading="lazy"。用font-display: swap提供 WOFF2 subsets。 - 在長頁面上,
content-visibility: auto可以跳過 offscreen rendering 工作。CSS containment 與 list virtualization 會改變行為——測量後再引入。 - Failure: 過度使用
preconnect/preload。preload會搶頻寬,且必須匹配最終 request 的 type 與 credentials mode。
7. Framework Rendering 與 Hydration
Client-rendered application 常常要等到 JavaScript、framework startup 與資料抓取完成,才有有意義的內容。Server rendering 把內容與資源引用移進 HTML,讓發現與 paint 更早發生。它並不會拿掉 JavaScript 成本。
Client render:
HTML shell → JS → Data → Paint
Server render + hydrate:
HTML with content → Early paint
HTML with content → JS download → Hydration
Early paint → Hydration- 頁面可以很快 paint,然後在大型 hydration task 跑的時候變得無回應。
- Streaming SSR、selective hydration、islands 與 Server Components 都針對同一個問題:減少 startup 工作,並把非關鍵 JavaScript 留在初始 dependency chain 之外。
- 問哪些內容必須在第一份 HTML 裡、哪些 components 需要 client JS、code 與 data 何時變得可發現、hydration 能否發生在更小的 boundaries,以及 framework 是否在製造 server-to-client request waterfalls。
- Failure: 把快速的 first paint 當成快速的頁面。Hydration 之後仍可能占住 main thread。
8. 測量這條 Path
用 production build。檢查 Network waterfall 與 Performance trace。
Navigation → TTFB → Resource load delay → Resource load duration → Element render delay → LCP
Resource load delay: late discovery / low priority → Expose in HTML earlier
Resource load duration: large asset / slow origin → Compress / CDN / resize
Element render delay: CSS / fonts / main thread → Unblock style and paint- Network 揭示晚到的發現、priority、queueing 與 transfer。Main 揭示 parsing、script execution、style、layout、paint 與 long tasks。Frames 揭示錯過的 deadlines。Experience 揭示 layout shifts 與 Core Web Vitals。
- 對緩慢的 Largest Contentful Paint,把時間拆成 TTFB、resource load delay、resource load duration,以及 element render delay。高 load delay → 發現或 priority。高 load duration → asset 大小或 origin。高 render delay → CSS、fonts、decoding、JavaScript,或 main-thread contention。
- FCP:第一份內容。LCP:最大的可見內容。CLS:意外移動。INP:互動延遲直到下一次 paint。
- Lab traces 解釋原因;real-user monitoring 顯示用戶遇到它們的頻率。
- Failure: 快速的 LCP 配上糟糕的 INP。Hydration 或 third-party scripts 可能在最大 paint 之後占住 main thread。
最終心智模型
Critical rendering path 消耗三種有限資源:用來發現並傳輸 critical bytes 的 network time,用來 parse、execute、style 與 lay out 的 main-thread time,以及用來 paint、rasterize 與 composite frames 的 rendering capacity。
快的頁面盡早暴露重要資源、避免意外 blockers、減少 startup JavaScript、預留 layout 空間,並把初始 viewport 之外的工作延後。目標是從 navigation 到用戶前來要看的 pixels 之間,最短的 dependency chain。