Skip to content

关键渲染路径 Critical Rendering Path

元信息

  • 目标:掌握浏览器从代码到像素的渲染流水线(DOM/CSSOM→渲染树→布局→绘制→合成),从此能从原理层面解释 defer/async、CSS 放头部、图片懒加载这些"惯例"的由来
  • 关键概念:关键渲染路径、DOM/CSSOM、渲染树、布局与回流、渲染阻塞、解析阻塞、async/defer、懒加载
  • 关联阶段:cp2
  • 常见误区:以为 CSS 阻塞 HTML 解析(CSS 只阻塞渲染成像,DOM 构建照常);以为脚本仍应放页面底部(defer 出现后放 head 既早下载又不阻塞);以为 display:none 的节点也进渲染树(不可见节点不进入)

CRP 是浏览器将 HTML、CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。理解它,才能理解为什么脚本要加 defer、为什么 CSS 要放头部、为什么图片要懒加载——前面各周遇到的"惯例",在这一篇里都能找到原理层面的答案。

渲染流水线

从收到字节到看见像素,主线路径是六步:

plain
字节 → 字符 → Tokens → Nodes(DOM/CSSOM) → 渲染树 → 布局 → 绘制 → 合成 → 像素
  1. 解析 HTML,构建 DOM 树。按代码顺序解析,标签转为树节点;遇到外联资源引用则发起 HTTP 请求(见HTTP协议
  2. 解析 CSS,构建 CSSOM 树。与 DOM 构建并行进行——谁也不用等谁,CSSOM 只由样式表构建,与 HTML 元素无关
  3. 构建渲染树。等 DOM 与 CSSOM 就绪后,把可见节点配上最终样式(可见——display:none 的节点不进入渲染树)
  4. 布局 Layout(回流 Reflow)。计算渲染树中每个元素的位置与大小
  5. 绘制 Paint。把每个盒子填充为图层上的文本、颜色、图像、边框
  6. 合成 Composite。将各图层按正确顺序合并输出到屏幕

前两步并行、第三步会师,是整条流水线的关键结构。此后 JS 改样式触发的也是这条链的局部重跑:改几何属性触发"布局→绘制→合成"(代价大),只改颜色等则跳过布局(代价小)。

解析过程中的其他角色

  • 预加载扫描器 preload scanner:主解析器逐行解析的同时,扫描器在后台预先扫全文,提前发出外联资源的请求——不必等主解析器"走到"引用处,资源已在路上
  • JavaScript 的编译与执行:JS 被解析为抽象语法树(AST),现代引擎(如 V8)进一步编译为字节码,在渲染主线程上解释/执行(web worker 等例外,见第 9 周
  • 无障碍树 AOM:基于 DOM 构建的无障碍对象模型,屏幕阅读器(见第 3 周语音浏览器)消费的就是它

阻塞 blocking

理想流水线会被资源之间的依赖关系打断,阻塞有三类:

渲染阻塞

CSS 是渲染阻塞资源:CSSOM 构建完成前,浏览器不会渲染任何内容(哪怕 DOM 已就绪)。原因是级联——不完整的样式表算不出"最终样式",先渲染再改等于闪一次错误的页面。

但这不等于 CSS 阻塞 HTML 解析:DOM 的构建照常进行,只是"成像"被推迟。

解析阻塞与执行阻塞

<script src="app.js"></script>(无属性)是解析阻塞资源:解析器停下,等脚本下载并执行完毕,才继续解析后续 HTML。为什么这么严格?因为脚本可能 document.write 改写后面的文档,也可能查询/修改已有元素,顺序错了结果就错。

连带效应:脚本执行前还必须等 CSSOM 就绪(脚本经常查询元素的计算样式),所以CSS 会间接阻塞其后的脚本

网络等待

外联资源都要走网络。解析器等待网络返回是"阻塞式";不等待、先继续解析则是"非阻塞式"——但非阻塞让资源就绪顺序变得不可预测,需要 async/defer 这类机制来声明依赖关系。

script 的三种加载模式

写法下载执行时机适用
<script src="a.js">阻塞解析,立即下载下载完立即执行,阻塞后续解析脚本依赖前面的 DOM 或脚本顺序
<script src="a.js" async>并行下载,不阻塞解析下载完立即执行,时点不可预测独立脚本(统计、广告),互不依赖
<script src="a.js" defer>并行下载,不阻塞解析HTML 解析完毕后按文档顺序执行依赖完整 DOM、需保持顺序的应用脚本

注意:asyncdefer 仅对外联脚本有效(内联脚本无此属性);defer 的存在让"脚本放 <head>"重新成为最佳实践——既早下载,又不阻塞、不怕元素未就绪。"把脚本塞到页面底部"是 defer 出现之前的时代应对,不必再效仿。

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

非关键资源的延迟

用户首先关心的是首屏内容,装饰性图片、视口外的图片不必急着加载:

  • 原生懒加载:<img src="photo.jpg" loading="lazy" />,浏览器自行推迟视口外图片的请求
  • 占位与渐进:给图片预留尺寸(width/height)避免加载后突然撑开文本;渐进式 JPEG 边下载边显示
  • 旧式手段是用 JS 把 data-src 换写为 src 实现懒加载,原生属性出现后已无需手写

字体

默认情况下字体请求会被推迟到渲染树构建时——样式表引用了某字体、且页面上确实用到它,才发起请求,这会导致文字先"隐形"再换装。可用 <link rel="preload"> 提前请求、font-display: swap 让浏览器先用回退字体显示,字体就绪后再替换。

优化 CRP 的三条原则

  1. 减少关键资源数量:异步(async/defer)或消除非关键资源
  2. 优化关键资源的顺序:让关键 CSS、应用脚本按依赖顺序尽早到达,缩短关键路径长度
  3. 减小资源体积:压缩、最小化,合理合并请求(如 css 精灵图、icon font,见图标字体

参考