FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

[译]渲染性能优化之Layout篇 #198

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago

原文链接:https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing

前置知识

Layout与Reflow的区别是什么?

没有本质区别,只是浏览器的叫法不同。 Layout和Reflow都是浏览器处理元素几何信息的过程的叫法,几何信息包括size,location等等通过css声明的几何信息。

只不过在Chrome,Opera,Safari和IE中叫做Layout;在Firefox中叫做Reflow。

TL;DR(Too long;Don't read)

tl;dr是什么意思?

tl;dr是Too Long; Didn't Read的简写,一般用在比较长篇幅的内容前面,后面跟着一段内容的简短总结。 主要作用就是告诉读者,这篇内容篇幅比较长,如果不想深入探讨或时间有限,可以看总结。

尽可能避免layout

当你改变style时,浏览器会检查是否有依赖layout的改变去做计算,也会检查是否有渲染树需要更新。改变”几何属性“,比如width,height,left,top都依赖于layout。

.box {
  width: 20px;
  height: 20px;
}

/**
 * 改变width和height
 * 触发 layout
 */
.box--expanded {
  width: 200px;
  height: 350px;
}

Layout的作用域往往是当前document。 如果你有很多元素,会花费很长时间去找到它们的位置和维度。 如果不能避免layout的话,可以使用devtools查看 layout花费的时间,然后去判断是否是layout引起的阻塞。Performance可以看到。

老版本devtools上时timeline,看下面这张图: image

layout花费了20多ms,当我们在屏幕上花费16ms得到一个frame时,就已经是算很高了。你同样可以看到,devtools将告诉你这个layout有多少个elements,以及有多少个node节点需要layout。

这里有一个查看改变属性时触发layout的网站:https://csstriggers.com/

width属性会触发layout,遭到破坏的元素需要重新绘制,然后再进行合成。 image

z-index虽然不会触发layout,但是会触发painting,这种操作是超级昂贵的。然后再重新合成。 image

新版的Performance工具: image

在旧的布局模型中使用flexbox

web有很多很多layout models,其中一些兼容性更好。老的CSS layout模型允许我们在屏幕上相对定位它的位置,绝对定位和浮动定位。

下面的截图显示了使用浮动定位1300个盒子时layout的消耗。当然,这个是故意为了演示float布局的高消耗才做的。

1300个节点,float布局花了14ms。 image

当我们使用Flexbox布局后,1300个节点只花了3.544ms。 image

这种损耗对比可能不用太在意,但是大脑中一定要牢记:”布局模型的选择会影响到性能,也是一个优化点。“

任何情况下,无论是否选择Flexbox,都要牢记避免一起触发layout。

避免强制同步layout

一个frame的生成包括以下这几步: image

首先是js执行,然后是样式计算,然后是布局。但是,可以用js强制浏览器更早地实现布局。 这个叫做强制同步布局(forced synchronous layout)

首先需要知道的是,由于JavaScript运行前一帧中的所有旧布局值都是已知的,可以查询到的。所以如果想在每一帧开始前写一个元素的height的话,,可以按照下面这样写:

// 在每一个frame开始前运行这个函数
requestAnimationFrame(logBoxHeight);

function logBoxHeight() {
  // Gets the height of the box in pixels and logs it out.
  console.log(box.offsetHeight);
}

如果你在问它的高度之前就改变了盒子的样式,那么问题就来了:

function logBoxHeight() {

  box.classList.add('super-big');

  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);
}

现在,为了回答高度问题,浏览器必须首先应用样式更改(因为添加了超级大的类),然后运行布局。也就是说需要执行JavaScript -> Style->Layout三步。因为是一个很大的class,在Style和Layout这两步可能会消耗很长的时间。 每一个选中的box都在首个frame渲染前都需要执行这一步,box一旦多起来,性能就会有明显影响了。 只有这样,它才能返回正确的高度。这是不必要的,而且可能是昂贵的工作。

因此,最好在写之前读,浏览器可以用前一帧的layout值。

function logBoxHeight() {
  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

大多数情况下不需要赋值然后查询,用最后一帧的值就足够了。 运行样式计算和同步布局可能会导致明显的性能瓶颈。

避免layout抖动

有一种更坏的强制同步layout的方法:短时间内做大量的layout。

function resizeAllParagraphsToMatchBlockWidth() {

  // Puts the browser into a read-write-read-write cycle.
  for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

这段代码循环遍历一组段落,并将每个段落的宽度设置为与名为box的元素的宽度匹配。它看起来是无害的,但问题是循环的每次迭代都读取一个样式值(box.offsetWidth),然后立即读取。

解决这个问题的办法是只读一次然后写:

// Read.
var width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = width + 'px';
  }
}

如果你想保证安全,你应该检查FastDOM,它会自动为你批量你的读和写,并应该防止你触发强制同步布局或布局抖动意外。