在前端开发中,我们经常听到前辈们说:“要尽量减少 DOM 操作,避免引起回流和重绘。”

这句话听起来像是一句放之四海而皆准的“政治正确”,但如果你去深究:为什么读一个 offsetHeight 就会引发性能灾难?为什么用 transform 做动画就比 margin-left 丝滑百倍?现代框架(如 React/Vue)在底层又是如何帮我们擦屁股的?

今天,我们就来扒一扒浏览器渲染引擎底层的这三个核心概念:回流(Reflow/Layout)、重绘(Repaint/Paint)与 合成(Compositing)

1. 渲染流水线:从代码到像素的旅程

在聊这三个概念之前,我们需要先达成一个共识:浏览器把 HTML/CSS 变成屏幕上的像素,是一条严格的流水线。精简来看,核心步骤如下:

  1. 构建树: DOM 树 + CSSOM 树 $\rightarrow$ 生成渲染树(Render Tree)。
  2. Layout(回流/布局): 计算每个节点在屏幕上的确切大小和位置
  3. Paint(重绘/绘制): 填充像素,画出节点的颜色、文字、阴影等视觉效果。
  4. Composite(合成): 把绘制好的图层按正确顺序叠放在一起,最终输出到屏幕。

性能优化的核心奥义就一句话:在修改页面时,尽可能跳过前面的步骤,直接走后面的步骤。


2. 回流 (Reflow) —— 牵一发而动全身的“核弹”

什么是回流? 当元素的几何属性(宽高、位置、隐藏/显示)发生改变时,浏览器需要重新计算渲染树中受影响节点的几何信息。这个过程在 Chrome 中被称为 Layout。

生动比喻: 想象一个排满人的大合影。如果你要在第一排中间硬塞进一个胖子(修改 DOM 或改变宽度),那么他后面的所有人,甚至旁边的人,都要重新调整站位。这就是回流。

触发条件:

  • 修改 widthheightpaddingmargin
  • 改变窗口大小(Resize)。
  • 隐形杀手: 当你用 JS 读取 offsetTopscrollWidthgetComputedStyle() 时。浏览器为了给你最精确的值,会强制清空当前的渲染队列,立刻触发一次回流

性能代价:极高。 回流是一个 $O(N)$ 级别的操作,DOM 树越复杂,卡顿越明显。并且,回流必定会触发重绘


3. 重绘 (Repaint) —— 换个马甲的“刷漆工”

什么是重绘? 当元素的外观属性发生变化,但没有改变其布局空间时,浏览器只需要重新把新的像素颜色涂上去。

生动比喻: 还是那个大合影,合影队形完全没变,只是其中一个人把红衣服换成了绿衣服。其他人都不需要动,摄影师只需要给这个人重新上色即可。

触发条件:

  • 修改 colorbackground-colorvisibility

性能代价:中等。 虽然省去了复杂的布局计算,但重新填充像素依然需要消耗 CPU 资源。


4. 合成 (Compositing) —— GPU 护航的“终极魔法”

什么是合成? 现代浏览器为了追求极致性能,引入了分层(Layer)的概念。它会把页面拆分成多个图层(类似 Photoshop 的图层),各自绘制后,再由 GPU 将它们合成在一起。

如果你修改的属性既不需要改变布局(无回流),也不需要重新填充像素(无重绘),仅仅是改变了图层的位置、缩放比例或透明度,浏览器就会直接跳过 Layout 和 Paint,进入 Composite 阶段。

触发条件:

  • transform(如 translate, scale, rotate)。
  • opacity

性能代价:极低。 这步操作完全交由 GPU 处理,GPU 天生就是做矩阵变换和透明度合成的王者,所以用 transform 做的动画永远比改 left/top 流畅。


5. 现代框架的降维打击:React 与 Vue 是怎么做的?

了解了底层的残酷,我们再来看看平时用的 ReactVue 是如何充当“性能保镖”的。

React:Virtual DOM 的批量更新艺术

在 jQuery 时代,我们要让一个列表增加 3 个元素,可能会执行 3 次 appendChild,触发 3 次回流。

React 引入了 Virtual DOM。你在代码里不管怎么 setState,React 都会先在内存里的 JS 对象树(虚拟 DOM)上进行模拟修改。 通过 Diff 算法(在 React 16+ 中进化为 Fiber 架构),React 会计算出这 3 个元素的最小变更集(Patch),然后合并成一次真实 DOM 的更新,最终只触发一次回流

Vue:精准打击与异步队列

Vue 的响应式系统在依赖收集时,精确知道了哪个组件的数据变了。 但如果同一个组件里的一个数据在一个事件循环(Tick)里被连续修改了 10 次,Vue 会触发 10 次回流吗? 不会。 Vue 内部维护了一个异步更新队列(基于 PromiseMutationObserver 这种微任务)。它会把这 10 次修改合并,直到当前同步代码执行完毕后,在 nextTick 中统一执行 DOM 更新。这就是所谓的“读写分离与批量更新”

总结

  1. 能用 CSS3 transformopacity 做的动画,绝不用 left/top(榨干 GPU)
  2. 尽量不要在 JS 的循环里频繁读取 offsetHeight 等属性。(避免强制同步布局)
  3. 如果一定要进行复杂的 DOM 操作,先让元素脱离文档流(display: none 或使用 DocumentFragment),操作完再放回去。

理解了回流、重绘与合成,你就掌握了前端性能优化的“上帝视角”。无论是手写原生 JS,还是用 Next.js 搭建复杂的 SSR 页面,你都能清楚地知道每一行代码在浏览器底层引发的蝴蝶效应。


Leave a comment