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

布局是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。根据所用的 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”按钮,然后与您的网站交互。当您停止记录时,将看到网站表现情况的详细分析:

DevTools 显示布局要较长时间。

在深入研究上例中的轨迹时,我们看到每一帧的布局占用了超过 28 毫秒,当我们有 16 毫秒的时间在屏幕上获取动画中的帧时,该时间太远了。您还可以看到,DevTools 将说明树的大小(本例中为 1618 个元素)以及需要布局的节点数(本例中为 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.
const width = box.offsetWidth;

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

如果要保证安全,不妨考虑使用 FastDOM,它会自动为您批处理读取和写入,应当能防止您意外触发强制同步布局或布局抖动。