避免大型、复杂的布局和布局抖动

布局是浏览器计算元素几何信息的过程,即元素的大小以及在页面中的位置。根据所使用的 CSS、元素的内容或父级元素,每个元素都将有显式或隐含的大小信息。此过程在 Chrome 中称为布局 (Layout)。

布局是浏览器计算元素几何信息的过程:元素的大小以及在页面中的位置。根据所使用的 CSS、元素的内容或父级元素,每个元素都将有显式或隐含的大小信息。此过程在 Chrome(以及 Edge 等衍生浏览器)和 Safari 中称为布局。在 Firefox 中称为自动重排 (Reflow),但其过程实际上是一样的。

与样式计算类似,布局开销的直接注意事项如下:

  1. 需要布局的元素数量,该数量是页面的 DOM 大小的副产品。
  2. 这些布局的复杂性。

摘要

  • 布局会直接影响互动延迟时间
  • 布局通常限定为整个文档。
  • DOM 元素的数量会影响性能;应尽可能避免触发布局。
  • 避免强制同步布局和布局抖动;先读取样式值,然后进行样式更改。

布局对交互延迟时间的影响

当用户与网页互动时,这些互动应尽可能快。完成互动所需的时间被称为“互动延迟”,从浏览器呈现下一帧到显示互动结果时结束,这称为互动延迟。这是“Interaction to Next Paint”指标衡量的网页性能的一个方面。

浏览器为响应用户互动而呈现下一帧所用的时间称为互动的呈现延迟时间。交互的目标是提供视觉反馈,以告知用户已发生的事情,而视觉更新可能需要一些布局工作才能实现该目标。

为了尽可能降低网站的 INP,请务必尽可能避免布局。如果无法完全避免布局,则限制布局工作,以便浏览器可以快速显示下一帧,这一点非常重要。

尽可能避免布局

当您更改样式时,浏览器会检查任何更改是否需要计算布局,以及是否需要更新渲染树。对“几何属性”(如宽度、高度、左侧或顶部)的更改都需要布局。

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

/**
  * Changing width and height
  * triggers layout.
  */

.box--expanded {
  width: 200px;
  height: 350px;
}

布局几乎总是将作用域限定为整个文档。如果元素数量很多,将需要很长时间才能确定所有元素的位置和尺寸。

如果无法避免布局,关键是再次使用 Chrome DevTools 查看用时,并确定布局是否是造成瓶颈的原因。首先,打开开发者工具,转到“Timeline”标签页,点击“record”按钮,然后与您的网站互动。停止记录后,您将看到网站效果的细分数据:

开发者工具在布局中显示很长时间。

在深入研究上例中的轨迹时,我们发现每一帧的布局占用了超过 28 毫秒,当我们有 16 毫秒的时间在屏幕上获取动画中的帧时,该时间太远了。您还可以看到,开发者工具会告知您树大小(在本例中为 1,618 个元素)以及需要布局的节点数量(在本例中为 5 个)。

请注意,此处的一般建议是尽可能避免布局,但并非总是可以避免布局。请注意,在无法避免布局的情况下,布局的开销与 DOM 的大小有关。虽然两者之间的关系并不紧密耦合,但较大的 DOM 通常会产生较高的布局开销。

避免强制同步布局

将帧发送到屏幕具有以下顺序:

使用 Flexbox 作为布局。

首先运行 JavaScript,然后执行样式计算和布局。不过,可以使用 JavaScript 强制浏览器提前执行布局。这称为强制同步布局。

首先要记住的是,当 JavaScript 运行时,来自上一帧的所有旧布局值都是已知的,并且可供您查询。例如,如果您想在帧的开头写出某个元素(我们称之为“框”)的高度,可以编写如下一些代码:

// 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('super-big');
}

在大多数情况下,您不需要先应用样式,然后再查询值;使用最后一帧的值应该就足够了。与浏览器同步且比其更早运行样式计算和布局可能是潜在的瓶颈,并且您通常不希望这么做。

避免布局抖动

有一种方式会使强制同步布局变得更加糟糕:连续快速执行很多布局。让我们来看看这段代码:

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

此代码会在一组段落中循环,并设置每个段落的宽度,使其与名为“box”的元素的宽度相匹配。这看起来没有什么危害,但问题是循环的每次迭代都会读取样式值 (box.offsetWidth),然后立即使用该值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下一次迭代时,浏览器必须考虑自上次请求 offsetWidth 以来(在上一次迭代中)已经更改样式的情况,因此它必须应用样式更改,然后运行布局。每次迭代时,都会发生这种情况!

此示例的解决方法是再次执行 read,然后执行 write 值:

// Read.
const width = box.offsetWidth;

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

如果您想保证安全性,不妨考虑使用 FastDOM,它会自动为您批量完成读取和写入操作,应该能够防止您意外触发强制同步布局或布局抖动。

主打图片来自 Hal GatewoodUnsplash 网站。