DOM – 浏览器 forced reflow (layout) 优化

前言

没有深入研究过,懂个概念就好,等以后遇到性能瓶颈再回来深入研究。

以前写的笔记:浏览器 reflow

 

参考:

reflow和repaint引发的性能问题

精读《web reflow》

 

浏览器渲染过程

浏览器绘制页面有好几个过程,比较常见的有:

image

  1. Recalculate style

    依据 CSS 把每个 element 各种 styling 计算好。

  2. Layout (以前叫 reflow)

    负责计算整体布局,每个 element 具体位置(BoundingClientRect)和尺寸,比如说 recalculate style 只知道 element width: 50%,但 50% 是多少?

    需要靠 layout 来计算。

  3. Pre-paint 和 Paint

    上面两个步骤只是计算,真正开始把它绘制出来是 Paint。

    而 Pre-paint 是对上 recalculate 和 layout 计算的结果做整理,提供给 paint。

    注:Paint 只是生成绘制指令,还没有到最终呈现的像素哦。

  4. Layerize

    对 paint 的结果做分层。

    如果不分层,每一次改动都可能要全部重绘,分层就可以做局部重绘。

  5. 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。

两个知识点:

  1. 不是说每一次调用 offsetWidth 都会导致 reflow

    reflow 的前提是要有 dirty(而且是针对 layout 的 dirty)

    因为我们修改了 width,所以它有 dirty,这时调用 offsetWidth 才会 reflow。

    reflow 之后 dirty 就 clear 掉了。

    假如我们再调用一次 offsetWidth,它就不会 reflow 了。

  2. reflow 不等于渲染

    渲染依然是依据 timing,提前 reflow 只是做 layout 计算,用户是看不见的。

    等 timing 到时,浏览器如果没有 layout dirty 它就不需要再 reflow,但渲染还是需要的。

Best practice

假设我们要 resize table 的每一个 column width。

如果流程是:

  1. for loop 每一个 column

  2. get widest cell

  3. update all column cell to widest

这样就不对,因为 for loop 的每一次都是一读一写,这样每一次都会 reflow。

正确流程应该是:

  1. for loop 读取每一个 column 的 widest cell

  2. 再 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;
  }
});

效果

gif

它不会有 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;

效果

gif

这时就会有 fade in 效果了,是不是挺奇妙的?

因为 forced reflow 的关系,实际上它 reflow 了两次。

一次是 display: block,一次是 opacity: 1。

我们查看 DevTools 就一目了然了

下图是没有读取 offsetHeight 的

image

它只有一次 reflow。

而有读取 offsetHeight 的图是这样

image

可以清楚的看到,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';
  }
});

效果

gif

同样它不会有 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';

于是,神奇的事情又发生了

gif

如果你觉得 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 呢?

答:会!

gif

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

image

keydown 后直接 animation frame fired

而有先点击屏幕的话,它的流程变成这样

image

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 效果

 

posted @ 2022-03-11 21:33  兴杰  阅读(102)  评论(0)    收藏  举报