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。