前五章讲「怎么写」,本章讲「写完之后浏览器怎么跑」。这是前端课程里最值得慢读的一章:渲染流水线解释「为什么改这个属性页面卡」、回流重绘解释「为什么框架要虚拟 DOM」、脚本加载三兄弟解释「为什么首屏白屏」,后面所有性能优化的原理依据都在这。素材参考 Chrome 团队的《Inside look at modern web browser》系列,配 F12 的 Performance 面板食用最佳。
浏览器拿到 HTML 后到屏幕出现像素,走五步:
解析 HTML → DOM 树 (结构)
解析 CSS → CSSOM 树 (样式)
合并 → 渲染树(可见节点;display:none 不入树)
布局 Layout:计算每个盒子的几何位置
绘制 Paint:逐层填充像素
合成 Composite:分层贴图上屏(GPU 参与)
这五步的变更成本从左到右递减,这是理解一切性能问题的钥匙:改 DOM 结构或布局属性,五步全重走(回流);只改颜色,跳过布局走后半程(重绘);只改 transform/opacity,连重绘都免,纯 GPU 合成。所以「动画性能优化」的第一戒律由此而来:动画只用 transform 和 opacity(translateX 而非 left,它不触发布局)。
| 回流(Reflow/Layout) | 重绘(Repaint) | |
|---|---|---|
| 触发 | 几何变化(尺寸、位置、增删节点) | 仅视觉变化(颜色、背景) |
| 代价 | 高(重新布局 + 重绘) | 低 |
| 例子 | 改 width、读 offsetWidth | 改 color |
回流里藏着 JS 世界最阴的性能陷阱:读写交错强制同步回流。布局计算是脏标记驱动的(攒一批一起算),但一旦你读几何属性(offsetWidth、getBoundingClientRect),浏览器必须立刻给你准确的值,只能当场把积压的布局算掉:
// 反例:写一次读一次,循环 100 次 = 强制回流 100 次
for (let i = 0; i < 100; i++) {
el.style.width = i + "px";
total += el.offsetWidth; // 每次都被迫同步布局!
}
// 正解:读写分离,先读光再写光
const w = el.offsetWidth; // 读:一次
el.style.width = w + 100 + "px"; // 写:一次,布局在下帧统一算
批量 DOM 操作同理:循环 createElement + append 每次都可能触发回流,先装进 DocumentFragment(离屏的容器)再一次 append。这些优化在 React 里被虚拟 DOM 自动化了(第 8 章):框架 diff 后一次性提交最小变更集,「为什么要有虚拟 DOM」的原始动机就在这。
JS 可以改 DOM,所以解析 HTML 遇到 <script src> 必须停下(防止后面的 DOM 被脚本改得面目全非),下载并执行完才继续。代价是白屏等待,于是两个补救属性:
<script src="a.js"></script> <!-- 普通:阻塞解析,立即执行,保序 -->
<script src="b.js" defer></script> <!-- 下载不阻塞,DOM 就绪后按序执行 -->
<script src="c.js" async></script> <!-- 下载不阻塞,下完立刻跑,不保序 -->
| 阻塞解析 | 执行时机 | 顺序 | |
|---|---|---|---|
| 普通 | 是 | 立即 | 保序 |
| defer | 否 | DOM 解析完 | 保序 |
| async | 否 | 下载完立即 | 不保序 |
选型:依赖 DOM 的应用脚本用 defer(现在的默认答案);独立统计脚本用 async;普通裸 script 基本只在内联关键代码时用。这个知识点直接对应首屏白屏问题(6.5)。
把计算机网络课(第 2 章)的 HTTP 知识接进来。地址栏回车后的完整旅程:DNS 解析 → TCP 握手 → TLS 握手 → HTTP 请求 → 响应 HTML → 解析中并发拉子资源(CSS/JS/图)→ 渲染。每个环节都是延迟来源,对应的优化:
DevTools 的 Network 面板是这一切的可视化:瀑布图里每条资源的时间线(排队、DNS、首字节、下载)逐段可查,配 Performance 面板看渲染帧,性能调优的工具闭环就在这。
「页面白屏 3 秒」的排查清单,本章知识全用上:Network 面板看 HTML 本身多久回来(网络问题,计网课)→ JS 是否裸 script 阻塞(defer 化)→ bundle 是否过大(代码分割,第 9 章)→ Performance 面板看长任务(JS 主线程被占死,第 7 章事件循环)。你会发现「前端性能」= 网络 + 渲染 + 主线程三条线,本章给的是渲染这条线。
performance.now() 分别测「纯写 100 次」和「写读交错 100 次」的耗时,差出一个量级。