精读「回流与重绘」
660 字 · 2 分钟#浏览器#性能
↑ 网页渲染流程。从顺序上可以看出,重排和重绘都会影响网页性能,且重排后一定重绘,而重绘不一定触发重排。
概述
什么时候会触发重排?一般来说,当元素位置发生变化时就会。但也不尽然——浏览器会自动合并更改,在达到某个数量或时间(一帧)后合并为一次重排。重排是渲染页面的重要一步,本身不可避免。
那为什么还要专门注意重排导致的性能问题?因为某些代码会让浏览器的合并优化失效:明明能合并的重排没有合并。典型的情况是访问元素尺寸——为了拿到精确值,浏览器必须提前触发一次重排。好在也不是每次访问都会触发:重排之后所有已有元素都会记录快照,只要不再发生位置变化,再次访问尺寸就不会重复触发。
触发条件
触发重排的属性和方法非常多,归纳起来主要是两类:读取盒模型信息(offsetTop、offsetWidth、clientHeight、scrollTop、getComputedStyle() 等,为了返回精确值必须先结算布局)和改变布局(几何属性变更、DOM 增删、窗口尺寸变化等)。完整清单可以参考 What forces layout / reflow。
解决办法
Avoid large complex layouts
核心是读写分离。在循环中交替地读取和修改元素宽度,会导致浏览器执行 N 次回流:JavaScript 运行时,前一帧的所有布局值都是已知的;可一旦你修改了布局,这份缓存就全部作废,下一次读取就不得不重新触发回流。
// 👎 读-写-读-写,每轮循环都强制一次回流
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
// 👍 先读后写,整个函数只有一次读取
function resizeAllParagraphsToMatchBlockWidth() {
const boxOffsetWidth = box.offsetWidth;
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = boxOffsetWidth + 'px';
}
}
另外还提到 flex 布局比传统 float 的重排速度快很多(3ms vs 16ms),所以能用 flex 做的布局就尽量不要用 float 做。
Really fixing layout thrashing
手动保证读写分离对纪律要求太高,尤其是读写逻辑分散在不同模块的时候。fastdom 的思路是在不重构代码组织的前提下分离读写的执行时机:每一个操作都会推入队列,统一在 window.requestAnimationFrame 时机按「先读后写」的顺序执行。
ids.forEach(id => {
fastdom.measure(() => {
const top = elements[id].offsetTop
fastdom.mutate(() => {
elements[id].setLeft(top)
})
})
})
总结
回流无法避免,但需要控制在正常频率范围内。我们需要知道访问哪些属性或方法会导致回流,尽量做到读写分离;在定义要频繁触发回流的元素时,尽量使其脱离文档流(position: absolute/fixed),把回流的影响范围限制在最小。