第 6 章 · 浏览器工作原理

前五章讲「怎么写」,本章讲「写完之后浏览器怎么跑」。这是前端课程里最值得慢读的一章:渲染流水线解释「为什么改这个属性页面卡」、回流重绘解释「为什么框架要虚拟 DOM」、脚本加载三兄弟解释「为什么首屏白屏」,后面所有性能优化的原理依据都在这。素材参考 Chrome 团队的《Inside look at modern web browser》系列,配 F12 的 Performance 面板食用最佳。

6.1 渲染流水线:从字节到像素

浏览器拿到 HTML 后到屏幕出现像素,走五步:

解析 HTML → DOM 树          (结构)
解析 CSS  → CSSOM 树        (样式)
合并 → 渲染树(可见节点;display:none 不入树)
布局 Layout:计算每个盒子的几何位置
绘制 Paint:逐层填充像素
合成 Composite:分层贴图上屏(GPU 参与)

这五步的变更成本从左到右递减,这是理解一切性能问题的钥匙:改 DOM 结构或布局属性,五步全重走(回流);只改颜色,跳过布局走后半程(重绘);只改 transform/opacity,连重绘都免,纯 GPU 合成。所以「动画性能优化」的第一戒律由此而来:动画只用 transform 和 opacity(translateX 而非 left,它不触发布局)。

6.2 回流与重绘:改一次,痛一次

回流(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」的原始动机就在这。

6.3 脚本与渲染:三兄弟 defer/async/普通

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)。

6.4 一次页面加载的网络账

把计算机网络课(第 2 章)的 HTTP 知识接进来。地址栏回车后的完整旅程:DNS 解析 → TCP 握手 → TLS 握手 → HTTP 请求 → 响应 HTML → 解析中并发拉子资源(CSS/JS/图)→ 渲染。每个环节都是延迟来源,对应的优化:

  • 缓存:强缓存(Cache-Control,不发起请求)与协商缓存(ETag/Last-Modified,304 只回头)两级,静态资源带 hash 文件名 + 长缓存是标配(第 9 章);
  • 并发限制:HTTP/1.1 每域名约 6 个并发连接,资源域名分片是老办法,HTTP/2 多路复用后单域名即可(第 2 章的队头阻塞知识在此兑现);
  • 关键渲染路径:CSS 阻塞渲染(没 CSSOM 没法画),所以「内联首屏关键 CSS、其余 defer」。

DevTools 的 Network 面板是这一切的可视化:瀑布图里每条资源的时间线(排队、DNS、首字节、下载)逐段可查,配 Performance 面板看渲染帧,性能调优的工具闭环就在这。

6.5 白屏与首屏:一个综合诊断

「页面白屏 3 秒」的排查清单,本章知识全用上:Network 面板看 HTML 本身多久回来(网络问题,计网课)→ JS 是否裸 script 阻塞(defer 化)→ bundle 是否过大(代码分割,第 9 章)→ Performance 面板看长任务(JS 主线程被占死,第 7 章事件循环)。你会发现「前端性能」= 网络 + 渲染 + 主线程三条线,本章给的是渲染这条线。

常见错误

  • 动画改 left/top。触发回流,GPU 合成才是通路,transform 代替。
  • 读写交错。6.2 的循环陷阱,Performance 面板里表现为一串紫色 Layout 瀑布。
  • 所有脚本裸加载。defer 不白用;白屏时先查它。
  • 以为改 color 也回流。分层记忆:改布局属性 = 回流,改视觉属性 = 重绘,改合成属性 = 什么都不触发。

练习

  1. 动手:Performance 面板实录:录一段「点击按钮改 100 次宽度」的性能,找到紫色 Layout 条和黄色 Paint 条;改成 transform 后重录,对比(截图留档)。
  2. 动手:验证读写交错:用 performance.now() 分别测「纯写 100 次」和「写读交错 100 次」的耗时,差出一个量级。
  3. 动手:三兄弟实验:写三个各 2MB 的脚本分别用三种加载,Network 面板观察下载与执行时序,画出三种瀑布图。
  4. 思考:虚拟 DOM 的原始动机是「批量提交减少回流」。结合第 8 章框架对比,讨论:现代框架(Solid/Svelte 编译时优化、React 编译器)把优化从运行时搬向编译时,是不是在承认「虚拟 DOM 只是当年 JS 引擎不够快时的折中」?