作者:Paul Lewis
译者:御坂 + Google Translate
原文:原文链接
布局是浏览器为元素定位的信息——它们在屏幕上的的大小和位置。根据它们的CSS、元素的内容或者父元素的内容,每一个元素都有其显式或隐式的布局信息。这个过程在Chrome,Opera,Safari和Internet Explorer中叫做Layout,而在Firefox中叫做Reflow,但过程相似。
类似于样式的计算,布局的计算消耗主要取决于:
- 需要布局的元素数量。
- 布局的复杂度。
- 布局几乎总是作用于整个文档,而不是某一部分。
- DOM元素的数量会影响性能;你应该尽可能避免触发布局计算。
- 评估布局模型的性能;新的Flexbox通常比旧的Flexbox或者基于float的布局更快。
- 避免强制同步布局和布局抖动;先读样式的值,再做样式的更改。
当你更改样式时,浏览器将检查是否有将导致布局被重新计算和DOM树被更新的更改。对于"地理信息"的更改,例如宽度高度和定位选项都将导致布局重新计算。
.box {
width: 20px;
height: 20px;
}
/**
* Changing width and height
* triggers layout.
*/
.box--expanded {
width: 200px;
height: 350px;
}
布局几乎总是作用于整个文档,而不是某一部分。所以如果你有一堆元素,浏览器将需要很长很长时间来为它们定位。
如果不可能避免布局计算,那么你应该打开Chrome开发者工具,然后看看它花了多长时间,并找找看布局计算是不是性能瓶颈的原因。要做这件事,你需要打开开发者工具,Timeline标签页,点击录制按钮,和你的网站做足够多的交互,然后停止录制,你就会看到你的网站的性能数据。
当对我们上面图中的例子进行分析时我们发现,浏览器渲染时超过20ms花在了布局上,其中16ms是在动画中把一个框架放到屏幕上,这看起来非常夸张。你也可以看到开发者工具提示DOM树的大小(在本例中是1618个元素),以及在这个布局中需要多少节点。
前端有一系列的布局模型,其中有一些已经被另一些所广泛支持。最老的CSS布局模型允许我们相对地或绝对地或浮动地在屏幕上定位一个元素(relatively, absolutely, floating)。下面,我们来看看性能:
这个屏幕快照向我们展示了当floating 1300个盒子的时候布局的开销:14.289ms。不可否认这是一个人为的例子,绝大多数应用都使用很多种方式来对齐元素。
如果我们把这个例子中的floating换成flexbox,一个更加新的技术,我们可以看到现在我们在布局上只花了3.544ms。
需要注意的是有些内容不能使用Flexbox,因为它被floats更好的支持,但是如果可以的话你应该至少调查一下布局对于你的性能的影响,并且选择一个开销最小的方案。
总之,在任何情况下,不管你是否使用flexbox,你都应该在你的应用运算密集时尝试避免触发布局重新计算。
渲染一个框架到屏幕基本上需要经历以下过程:首先JavaScript运行,然后计算样式,然后处理布局。 JavaScript - Style - Layout - Paint - Composite
但是,如果用户愿意,也可以用JavaScript来更早地处理布局。这叫做强制同步布局。不这样做的原因是,JavaScript上一次渲染(原文:previous frame)时的布局缓存对于你的请求始终已知和可用,读取它们时,浏览器是从缓存读取的而非计算得来。
接下来举例。比如说你想在每次渲染的开始读一个叫"box"的元素的高度,你可能会写这样的代码:
// Schedule our function to run at the start of the 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);
}
现在要获取高度的话浏览器必须先应用样式更改(因为我们添加了一个叫super-big
的类),然后布局。只有这样浏览器才能返回正确的高度。这很没必要而且可能很耗时。
因此你应该始终先读后写,这样浏览器就能用上一次渲染的结果缓存。
正确的方法应该是:
function logBoxHeight() {
// Gets the height of the box in pixels
// and logs it out.
console.log(box.offsetHeight);
box.classList.add('uper-big');
}
在大多数情况下,你都不应该先应用样式然后再获取值,使用上一次渲染的结果就足够。在浏览器之前做样式计算和布局同步是一个潜在的瓶颈,一般而言你都不会想这么做的。
强制布局同步已经很糟糕了,但是,我们还可以通过快速连续执行它们来让事情变得更糟。比如说:
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
,然后立即使用它来更新段落宽度。这样,每个循环后浏览器都要考虑box的宽度是否有变化,然后计算样式和布局。每个循环都会这么做。
要避免这个问题,我们需要缓存宽度值。
// Read.
var width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
// Now write.
paragraphs[i].style.width = width + 'px';
}
}
最后,如果你想要保证安全的话,你应该看看这个自动处理你的读写并且应该能防止你意外引发强制布局同步和布局抖动的工具:FastDOM