跳至主要內容

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 並重複這些階段。


text
HTML → DOM  ─┐
             ├→ Style → Layout → Paint → Compositing → Pixels
CSS  → CSSOM ┘

text
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。


html
<link rel="stylesheet" href="/app.css" />
<script>
  console.log(getComputedStyle(document.body).color)
</script>

ScriptExecutionOrdering
ClassicBlocks parserDocument order
deferAfter parsingDocument order
asyncAs soon as readyLoad order
type="module"Deferred by defaultModule dependency order

text
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。


ts
for (const card of cards) {
  card.style.width = "50%"
  console.log(card.offsetWidth)
}

text
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 變得可用。


text
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。


html
<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 成本。


text
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。


text
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。


Recap Q&A

閱讀下一篇筆記
React 19 新功能