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

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

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

与样式计算类似,布局开销的直接关注点如下:

  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 开发者工具来查看需要多长时间,并确定布局是否是造成瓶颈的原因。首先,打开开发者工具,前往“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,它可以自动为您批处理读取和写入,应该可以防止您意外触发强制同步布局或布局抖动。

主打图片来自 Unsplash 用户,由 Hal Gatewood 提供。