在开发复杂的前端交互时,你大概率遇到过这样的挫败感:明明只写了几十行 JS 去实现一个列表项的拖拽或者滚动加载动画,结果整个页面卡得像播 PPT 一样。打开 Chrome 的 Performance 面板一录制,长长的红色预警赫然标示着两个单词:Layout Thrashing (布局抖动)。
导致网页卡顿的头号杀手,往往不是你的业务逻辑写得不够精妙,而是你无意中疯狂地触发了浏览器的重绘(Repaint)与回流(Reflow/Layout)。要写出丝滑的 60fps 动画和流畅的页面,我们必须深入了解这两个渲染底层的核心概念。
当浏览器拿到 HTML 和 CSS 后,想要把它渲染成屏幕上的像素,大体会经历以下几个步骤这四个字构成的流水线: 构建 DOM 树 -> 构建 CSSOM 树 -> 生成渲染树 (Render Tree) -> 布局 (Layout/Reflow) -> 绘制 (Paint) -> 合成 (Composite)。
我们常说的“回流”和“重绘”,就是在页面发生更新时,浏览器不得不退回并重新走一遍最后这几个极其耗费性能的步骤。
当页面中元素的几何尺寸、位置或者结构发生改变时,浏览器需要重新计算整个文档树中受影响的元素的几何信息,并重新排列它们。这个过程极其昂贵,因为它不仅计算自己,通常还要连带着重新计算父元素、子元素甚至兄弟元素的位置。
触发回流的高危动作:
width、height、padding、margin、border、top、left 等)。resize)。很多新手不知道,仅仅是“读取”元素的某些属性,也可能强迫浏览器立即执行一次昂贵的回流!
由于浏览器对回流是有优化机制的(它会把修改操作放进一个队列里,等积攒够了或者到下一帧时一起执行),但是!如果你在 JS 中刚刚修改了样式,紧接着又去请求读取 offsetWidth、clientHeight、scrollTop、getComputedStyle() 等属性,为了能够准时返回给你最精确的最新数值,浏览器不得不立刻清空渲染队列,强行、立刻进行一次完整的回流。如果你把这种“读+写”的操作放进一个 for 循环里,那就是性能灾难。
当元素的外观、风格发生改变,但**并没有改变它的几何布局(没有改变占地面积)**时,浏览器只需要把该元素的新外观重新画一遍即可,这个过程耗费的性能相对较小。
触发重绘的操作:
color、background、visibility。box-shadow、border-radius。💡 核心定律:回流必将引起重绘!但重绘不一定引起回流。 这就像拆墙重建必定需要重新刷漆,但如果只是换个壁纸,并不需要把墙推倒。
这不仅是一道理论题,更是实实在在的工程踩坑点。让我们亲眼看看“读写混合”(Layout Thrashing)是如何引发性能灾难,而“读写分离”又是如何拯救世界的。
(你可以多次点击按钮对比毫秒数,读写混合在大量 DOM 或低性能设备上往往比读写分离慢一个数量级以上。)
除了代码层面上严格的“读写分离”(通常可以借助像 fastdom 等库来统一收口外),我们还能在 CSS 层面做出以下努力:
display: none 把这块 DOM 彻底藏起来(此时回流 1 次)。等在背后把几百次修改都做完后,再 display: block 把它请出来(再回流 1 次)。transform (translate, scale, rotate) 来代替 top/left/width 去做动画位移和缩放。opacity 来代替 visibility/display 去做透明度渐变动画。will-change: transform 或 transform: translateZ(0) 来显式将其提拔为一个独立的 GPU 合成层。Q:说说你对重绘和回流的理解?在 React/Vue 项目中如果遇到动画或循环修改样式严重卡顿,你会如何排查并解决?
回答思路:
display: none 或使用虚拟片段 DocumentFragment),修改完再塞回去。top/left,全盘改用 transform 并开启硬件加速交由 GPU 处理重构,彻底绕过主线程的回流深坑。