跳到主要内容

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 新功能