Appearance
关键渲染路径 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) → 渲染树 → 布局 → 绘制 → 合成 → 像素- 解析 HTML,构建 DOM 树。按代码顺序解析,标签转为树节点;遇到外联资源引用则发起 HTTP 请求(见HTTP协议)
- 解析 CSS,构建 CSSOM 树。与 DOM 构建并行进行——谁也不用等谁,CSSOM 只由样式表构建,与 HTML 元素无关
- 构建渲染树。等 DOM 与 CSSOM 都就绪后,把可见节点配上最终样式(可见——
display:none的节点不进入渲染树) - 布局 Layout(回流 Reflow)。计算渲染树中每个元素的位置与大小
- 绘制 Paint。把每个盒子填充为图层上的文本、颜色、图像、边框
- 合成 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、需保持顺序的应用脚本 |
注意:async 与 defer 仅对外联脚本有效(内联脚本无此属性);defer 的存在让"脚本放 <head>"重新成为最佳实践——既早下载,又不阻塞、不怕元素未就绪。"把脚本塞到页面底部"是 defer 出现之前的时代应对,不必再效仿。
非关键资源的延迟
用户首先关心的是首屏内容,装饰性图片、视口外的图片不必急着加载:
- 原生懒加载:
<img src="photo.jpg" loading="lazy" />,浏览器自行推迟视口外图片的请求 - 占位与渐进:给图片预留尺寸(width/height)避免加载后突然撑开文本;渐进式 JPEG 边下载边显示
- 旧式手段是用 JS 把
data-src换写为src实现懒加载,原生属性出现后已无需手写
字体
默认情况下字体请求会被推迟到渲染树构建时——样式表引用了某字体、且页面上确实用到它,才发起请求,这会导致文字先"隐形"再换装。可用 <link rel="preload"> 提前请求、font-display: swap 让浏览器先用回退字体显示,字体就绪后再替换。
优化 CRP 的三条原则
- 减少关键资源数量:异步(async/defer)或消除非关键资源
- 优化关键资源的顺序:让关键 CSS、应用脚本按依赖顺序尽早到达,缩短关键路径长度
- 减小资源体积:压缩、最小化,合理合并请求(如 css 精灵图、icon font,见图标字体)
参考
- 渲染页面:浏览器的工作原理 https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work
- 图解浏览器的工作原理(InfoQ 中文版) https://www.infoq.cn/article/CS9-WZQlNR5h05HHDo1b
- 浏览器是如何工作的(web.dev 原文) https://web.dev/articles/howbrowserswork
- Chromium 渲染流水线——字节码到像素的一生 https://blog.ursb.me/posts/chromium-renderer/
- 电子书《Web Browser Engineering》——用 Python 写一个真浏览器 https://browser.engineering/
- 关键渲染路径 https://developer.mozilla.org/zh-CN/docs/Web/Performance/Critical_rendering_path
- 懒加载 https://developer.mozilla.org/zh-CN/docs/Web/Performance/Lazy_loading
- script元素 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script
- Chrome V8 让你更懂 JavaScript https://king-hcj.github.io/2020/10/05/google-v8/