布局性能优化指南

作者:林语者 分类:工程代码

布局性能优化指南

发布日期:2015年3月20日,最后更新日期:2025年5月7日

在网页渲染过程中,布局是指浏览器计算元素的几何信息(尺寸和页面中的位置)的阶段。每个元素都拥有基于使用的CSS、元素内容以及父元素的显式或隐式尺寸信息。这个过程在Chrome(及基于Chromium的Edge等衍生浏览器)和Safari中被称为布局,而在Firefox中则称为“重排”,但两者的实质过程是相同的。

与样式计算类似,布局性能的主要关注点包括:

  • 需要布局的元素数量(这是页面DOM大小的直接结果)
  • 布局的复杂程度

核心要点

  • 布局直接影响交互延迟
  • 布局通常作用于整个文档
  • DOM元素数量会影响性能,应尽量避免触发布局
  • 为避免强制同步布局和布局抖动,应先读取样式值再进行样式修改

布局对交互延迟的影响

当用户与页面交互时,操作应尽可能快速完成。从交互开始到浏览器显示下一帧并展示交互结果所需的时间称为交互延迟。这是Interaction to Next Paint(INP)指标衡量的页面性能要素之一。

用户操作后浏览器显示下一帧的时间被称为操作显示延迟。交互的目的是通过视觉反馈告知用户操作已生效。为实现视觉更新,通常需要完成一定量的布局工作。

为保持较低的INP值,避免布局至关重要。如果无法完全避免布局,也应限制布局工作量,确保浏览器能快速渲染下一帧。

// 示例:不推荐的写法,会导致强制同步布局
function updateElement() {
  // 先修改样式
  element.style.width = '200px';
  
  // 接着读取布局信息,这会触发强制布局
  const height = element.offsetHeight;
  
  // 后续操作...
}

有关Interaction to Next Paint的详细信息,请参考相关文档。

尽可能避免布局

修改样式时,浏览器需要检查渲染树是否需要更新,以及是否需要重新计算布局。修改widthheightlefttop等几何属性都会触发布局。

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

/**
 * 修改宽度和高度
 * 会触发布局
 */
.box--expanded {
  width: 200px;
  height: 350px;
}

布局通常作用于整个文档。如果元素数量众多,计算所有元素的位置和尺寸将耗费大量时间。

如果无法避免布局,可以使用Chrome DevTools分析布局耗时,判断布局是否为性能瓶颈。打开DevTools,进入Performance面板,点击录制按钮,然后操作网站。停止录制后即可查看详细的性能分析数据。

当在DevTools的Layout部分看到较长时间时,说明布局可能成为性能瓶颈。例如,如果每帧布局耗时超过28毫秒,而动画需要在16毫秒内完成帧渲染,这个时间就过长了。

虽然通常建议尽量避免布局,但有时无法完全避免。需要注意的是,布局成本与DOM大小相关——DOM越大,布局成本通常越高。

避免强制同步布局

帧渲染的基本流程如下:

  1. JavaScript执行
  2. 样式计算
  3. 布局

然而,通过JavaScript可以强制浏览器提前执行布局,这被称为强制同步布局(或强制重排)。

首先需要注意的是,JavaScript执行时,可以访问前一帧的旧布局值。例如:

// 安排函数在帧开始时执行
requestAnimationFrame(logBoxHeight);

function logBoxHeight() {
  // 获取元素高度(像素)并输出
  console.log(box.offsetHeight);
}

但如果在查询高度前修改了样式,就会产生问题:

function logBoxHeight() {
  box.classList.add('super-big');
  // 此时浏览器必须先应用样式变更,执行布局,然后才能返回正确高度
  console.log(box.offsetHeight);
}

为了回答高度查询,浏览器必须先应用样式变更(因为添加了super-big类),然后执行布局,最后才能返回正确高度。这会带来不必要的工作和性能开销。

重要提示: 虽然上面的例子使用了offsetHeight属性,但实际上有许多属性会触发强制同步布局。因此,样式读取应批量处理,遵循先读取(使用前一帧的布局值)后写入的原则。

优化后的代码:

function logBoxHeight() {
  // 先读取(使用前一帧的值)
  console.log(box.offsetHeight);
  // 再写入
  box.classList.add('super-big');
}

在大多数情况下,使用前一帧的值就足够了。强制浏览器提前同步执行样式计算和布局可能成为性能瓶颈,通常不建议这样做。

避免布局抖动

比强制同步布局更糟糕的是在短时间内连续执行多次布局。考虑以下代码:

function resizeAllParagraphsToMatchBlockWidth() {
  // 将浏览器置于读写读写循环中
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = `${box.offsetWidth}px`;
  }
}

这段代码循环遍历段落组,将每个段落的宽度设置为与”box”元素相同的宽度。问题在于,循环的每次迭代都会读取样式值(box.offsetWidth),然后立即使用该值更新段落宽度(paragraphs[i].style.width)。在下一次迭代中,浏览器会发现自上次请求offsetWidth后样式已更改,因此需要应用样式变更并执行布局。这种情况会在每次迭代中重复发生。

修正方法:先批量读取值,再进行写入操作:

// 读取阶段
const width = box.offsetWidth;

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

识别强制同步布局和布局抖动

DevTools提供了强制重排分析工具,可以快速识别强制同步布局的情况。

此外,还可以使用Long Animation Frame API的forcedStyleAndLayoutDuration属性在现场识别强制同步布局。

标签: 性能优化

评论

发表评论

正在加载评论...