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

发布时间:2015 年 3 月 20 日,上次更新时间:2025 年 5 月 7 日

在布局阶段,浏览器会确定元素的几何图形信息:它们在网页中的大小和位置。每个元素都会具有显式或隐式尺寸信息,具体取决于所使用的 CSS、元素的内容或父元素。在 Chrome(以及 Edge 等派生浏览器)和 Safari 中,该过程称为“布局”。在 Firefox 中,此过程称为“Reflow”,但实际上是相同的。

与样式计算类似,布局费用方面的直接问题包括:

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

摘要

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

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

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

浏览器响应用户互动并呈现下一帧所需的时间称为互动的呈现延迟时间。互动的目标是提供视觉反馈,以向用户表明某些操作已发生,而视觉更新可能需要进行一些布局工作才能实现这一目标。

为了尽可能降低网站的 INP,请务必尽可能避免出现布局问题。如果无法完全避免布局,请务必限制布局工作,以便浏览器能够快速呈现下一个帧。

尽可能避免布局

当您更改样式时,浏览器会检查是否有任何更改需要计算布局,以及是否需要更新该渲染树。对“几何图形属性”(例如 widthheightlefttop)所做的更改都需要布局。

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

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

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

布局几乎总是作用于整个文档。如果您有许多元素,则需要花很长时间才能确定所有元素的位置和尺寸。

如果无法避免布局,关键在于再次使用 Chrome 开发者工具查看布局所需的时间,并确定布局是否是瓶颈原因。首先,打开开发者工具,前往“时间轴”标签页,点击“录制”,然后与您的网站互动。停止录制后,您会看到有关网站表现的详细数据:

显示多个紫色布局块的 DevTools。
Chrome 开发者工具在“布局”中显示很长时间。

深入研究上例中的轨迹后,我们发现每个帧在布局中花费的时间超过 28 毫秒,而我们只有 16 毫秒的时间来让动画中的帧显示在屏幕上,这显然太长了。您还可以看到,DevTools 会告知您树的大小(在本例中为 1,618 个元素)以及需要布局的节点数量(在本例中为 5 个)。

请注意,这里的一般建议是尽可能避免布局,但并非始终都能避免布局。如果您无法避免布局,请注意布局开销与 DOM 的大小有关。虽然这两者之间没有紧密耦合,但 DOM 越大,布局开销通常就越高。

避免强制同步布局

将帧发送到屏幕的顺序如下:

使用 Flexbox 作为布局。
渲染步骤

首先运行 JavaScript,然后进行样式计算,然后进行布局。不过,您可以使用 JavaScript 强制浏览器提前执行布局。这称为强制同步布局(有时也称为强制重新流布局)。

首先要注意的是,当 JavaScript 运行时,您可以查询上一个帧中的所有旧布局值。例如,如果您想在帧开始时输出某个元素(假设为“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('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.
const width = box.offsetWidth;

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

识别强制同步布局和抖动

开发者工具提供了强制重新流式传输数据分析,可帮助您快速发现强制同步布局(也称为“强制重新流式传输”)的情况:

显示强制重新流布局数据分析的 DevTools,其中指出导致强制重新流布局的函数是名为“w”的函数。
Chrome 开发者工具强制重新流布局数据分析。

您还可以使用 forcedStyleAndLayoutDuration 属性通过 Long Animation Frame API 脚本归因在该字段中识别强制同步布局。