DOM – 浏览器 forced reflow (layout) 优化
前言
没有深入研究过,懂个概念就好,等以后遇到性能瓶颈再回来深入研究。
以前写的笔记:浏览器 reflow
参考:
浏览器渲染过程
浏览器绘制页面有好几个过程,比较常见的有:

-
Recalculate style
依据 CSS 把每个 element 各种 styling 计算好。
-
Layout (以前叫 reflow)
负责计算整体布局,每个 element 具体位置(BoundingClientRect)和尺寸,比如说 recalculate style 只知道 element width: 50%,但 50% 是多少?
需要靠 layout 来计算。
-
Pre-paint 和 Paint
上面两个步骤只是计算,真正开始把它绘制出来是 Paint。
而 Pre-paint 是对上 recalculate 和 layout 计算的结果做整理,提供给 paint。注:Paint 只是生成绘制指令,还没有到最终呈现的像素哦。
-
Layerize
对 paint 的结果做分层。
如果不分层,每一次改动都可能要全部重绘,分层就可以做局部重绘。
-
Commit,Composite
把所有信息提交给合成器,合成器会组合绘制所有图层。
本编主要讲的是 Layout。
浏览器何时渲染(layout paint 绘制页面)?
当我们用 DOM API 修改 element styles 后,浏览器并不会立刻渲染。
它会 mark as dirty,等一个 timing。
timing 到了才会渲染(layout paint 绘制页面)。
这个 timing 是不固定的,它会依据各种 CPU / GPU 繁忙程度等因素去决定。
我们可以透过 requestAnimationFrame 拦截到这个 timing(渲染前的一刻),在渲染前做最后修改。
Forced reflow (layout)
有传言:getBoundingClientRect、getComputedStyle、offsetWidth 等会导致 reflow。
害我以前都不太敢用...
其实它的原理是这样:
一般情况:我们修改 element width,浏览器 mark as dirty,等 timing 做 reflow。
offsetWidth 情况:
我们修改 element width,浏览器 mark as dirty,等 timing 做 reflow。
在等待期间,我们调用 offsetWidth,浏览器为了要给我们准确的答案,它就必须提前 reflow(不等 timing 了)。
这就是为什么会说:调用 offsetWidth 会导致 reflow。
两个知识点:
-
不是说每一次调用 offsetWidth 都会导致 reflow
reflow 的前提是要有 dirty(而且是针对 layout 的 dirty)
因为我们修改了 width,所以它有 dirty,这时调用 offsetWidth 才会 reflow。
reflow 之后 dirty 就 clear 掉了。
假如我们再调用一次 offsetWidth,它就不会 reflow 了。
-
reflow 不等于渲染
渲染依然是依据 timing,提前 reflow 只是做 layout 计算,用户是看不见的。
等 timing 到时,浏览器如果没有 layout dirty 它就不需要再 reflow,但渲染还是需要的。
Best practice
假设我们要 resize table 的每一个 column width。
如果流程是:
-
for loop 每一个 column
-
get widest cell
-
update all column cell to widest
这样就不对,因为 for loop 的每一次都是一读一写,这样每一次都会 reflow。
正确流程应该是:
-
for loop 读取每一个 column 的 widest cell
-
再 loop 一次 update all column cell to widest
这样就从
"读写 → 读(forced reflow)写 → 读(forced reflow)写 → timging reflow"(reflow 了很多次)
变成了
"读读读 → 写写写 → timing reflow"(只 reflow 一次,完全没有 forced reflow)
妙用 forced reflow
有一个 h1
<h1 style="display: none; opacity: 0; transition: opacity 5s;">Hello World</h1>
一开始它是 display: none 和 opacity: 0。
我们同时让它 display: block 和 opacity: 1。
const h1 = document.querySelector('h1');
document.addEventListener('keydown', e => {
if(e.key === 'a') {
h1.style.display = 'block';
h1.style.opacity = 1;
}
});
效果

它不会有 fade in 的效果,因为浏览器会同时处理 display: block 和 opacity: 1,而 display:node 到 block 是不会有 transition 的。
正确的做法是分两段,先让它 display: block,然后再 opacity: 1。
我们在两行代码之间加入一些会导致 forced reflow 的代码
h1.style.display = 'block';
document.body.offsetHeight; // forced reflow
h1.style.opacity = 1;
效果

这时就会有 fade in 效果了,是不是挺奇妙的?
因为 forced reflow 的关系,实际上它 reflow 了两次。
一次是 display: block,一次是 opacity: 1。
我们查看 DevTools 就一目了然了
下图是没有读取 offsetHeight 的

它只有一次 reflow。
而有读取 offsetHeight 的图是这样

可以清楚的看到,offsetHeight 这个操作导致了 reflow,总共也 Commit 了两次。
在一个例子:
<h1 style="transition: height 1s; overflow: hidden;">Hello World</h1>
一开始是 height: auto
我们把它设置成 0px
const h1 = document.querySelector('h1');
document.addEventListener('keydown', e => {
if(e.key === 'a') {
h1.style.height = '0px';
}
});
效果

同样它不会有 transition,因为 height: auto 到 0px 是不会有 transition 的。
正确的做法是
h1.style.height = `${ h1.scrollHeight }px`;
h1.style.height = '0px';
先给它一个高度,然后才去 0px。
但这样还不够,因为 reflow 只有一次,它直接就 0px 了。
我们在中间加一个 forced reflow
h1.style.height = `${ h1.scrollHeight }px`;
document.body.offsetHeight;
h1.style.height = '0px';
于是,神奇的事情又发生了

如果你觉得 forced reflow 太邪门,可以使用比较正规的方式 -- requestAnimationFrame
h1.style.height = `${ h1.scrollHeight }px`;
requestAnimationFrame(() => requestAnimationFrame(() => h1.style.height = '0px'));
原理就是等它 timing reflow 之后再设置。
意想不到的 reflow
无意间发现一些有趣的场景
<h1 style="display: none; opacity: 0; transition: opacity 5s;">Hello World</h1>
const h1 = document.querySelector('h1');
document.addEventListener('keydown', e => {
if(e.key === 'a') {
h1.style.display = 'block';
h1.style.opacity = 1;
}
});
请问会有 transition 吗?
答:不会
那加上 requestAnimationFrame 呢?
h1.style.display = 'block';
window.requestAnimationFrame(() => {
h1.style.opacity = 1;
});
答:还是不会
那在 keydown 之前,先点击一下屏幕,然后才 keydown 呢?
答:会!

原因不详,我只知道在没有点击的情况下,keydown 的过程是

keydown 后直接 animation frame fired
而有先点击屏幕的话,它的流程变成这样

click 之后做了一个 schedule style recalculation,它只是 schedule 没有立刻执行 calcuration。
等到 keydown 之后它才执行 recalculate style,然后才到 animation frame fired。
keydown 和 animation frame fired 中间多了这几个步骤,导致了 reflow 2 次,所以 transition 就出现了。
总结
reflow 伤性能,要尽量避开 forced reflow。
先把需要的信息一次性读出来,然后才修改 element styles,这样就能有效的避开无畏的 forced reflow。
Q & A
1. querySelector 会导致 forced reflow 吗?
不会,因为 querySelector 和布局、尺寸、位置无关,它只是遍历 DOM 节点就可以了。
2. getComputedStyle() 没有导致 forced reflow?
准确的说,是读取 ComputedStyle 属性才会 forced reflow。
h1.classList.add('showing');
window.getComputedStyle(h1).color; // forced reflow
h1.classList.add('shown');
// 会有 fadein 效果

浙公网安备 33010602011771号