Posted in

【第 3323 期】性能优化:布局抖动和强制回流_AI阅读总结 — 包阅AI

包阅导读总结

1. 关键词:Web 性能优化、布局抖动、强制回流、渲染树、异步布局

2. 总结:本文介绍了 Web 性能优化中的布局抖动和强制回流问题,包括其概念、产生原因和避免策略,还提及了在 React 开发中的注意事项,强调开发者要关注浏览器样式和布局引擎的使用,以优化用户体验。

3. 主要内容:

– 布局与回流概念

– 介绍了布局在不同浏览器中的称呼及过程。

– 解释了异步布局和强制同步布局。

– 渲染树缓存和增量更新

– 网页加载时 DOM 解析,布局引擎计算元素样式和几何信息,通过渲染树缓存。

– DOM 变动会导致缓存失效,回流通常是增量的。

– 同步回流与布局抖动

– 展示使渲染树失效并强制回流的代码片段。

– 解释布局抖动的概念和危害。

– 避免布局抖动的策略

– 批量读取和写入。

– 注意可能触发回流的浏览器 API。

– React 开发中注意 useEffect 和 useLayoutEffect 钩子的使用。

思维导图:

文章地址:https://mp.weixin.qq.com/s/5OICXyMbyr-P3jdSQoIWjw

文章来源:mp.weixin.qq.com

作者:ikoofe

发布时间:2024/7/23 0:00

语言:中文

总字数:3594字

预计阅读时间:15分钟

评分:87分

标签:性能优化,前端开发,布局抖动,强制回流,浏览器渲染


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

前言

介绍了 Web 性能优化中的布局抖动和强制回流问题,并提供了避免这些问题的策略。今日前端早读课文章由 @ikoofe 翻译分享,公号:KooFE 前端团队。

正文从这开始~~

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

异步布局

当页面更改样式时,浏览器会检查这些变更是否需要计算布局,以及是否需要更新渲染树。为了在屏幕上渲染出一帧的内容,浏览器会首先运行 JavaScript,然后计算样式,最后运行布局。我们把这种布局方式称之为异步布局 (Asynchronous Layout),也被称作异步回流 (Asynchronous Reflow)。

强制同步布局

还有另外一种布局方式,当执行某些 JavaScript 代码时,会强制浏览器提前执行布局,以便获取元素的样式信息和精确坐标。比如,Element.getBoundingClientRect()同步返回 bounding box 信息和坐标信息:

 const element = document.getElementById("item1");

const rect = element.getBoundingClientRect();

console.log(rect);

如果请求的元素的坐标或样式尚未计算出来,浏览器必须立即通过执行回流来计算它们。这种立即调用样式和布局引擎来解析未确定坐标的情况被称为强制同步布局(或强制同步回流)。

在 Chrome 浏览器中,使用开发者工具来分析性能,在火焰图中可以看到强制同步回流:

将鼠标移到这些任务上,还会给出下面的提示信息:Forced reflow is a likely performance bottleneck.

Render Tree 缓存和增量更新

当网页开始加载时,DOM 被解析,这个阶段元素还没有视觉样式或位置信息。浏览器的布局引擎必须在把这些元素绘制到屏幕上之前,为每个可见元素计算样式和几何信息。

浏览器会尽可能地降低重排的成本。在布局完成之后,它通过将每个可见元素的样式和位置存储和缓存到浏览器内部数据结构中(称为渲染树,Render Tree)。

以一个简单的静态 HTML 网页为例,来讨论这个缓存的机制。

  • HTML 被解析为 DOM。所有元素都是没有定位和样式。

  • 浏览器执行异步回流,为每个可视元素分配样式和坐标。

  • 样式和坐标被组合成渲染树,样式信息被缓存以供后续访问使用。

一旦样式和坐标被缓存,后续对样式和几何信息的访问速度非常快。在我们的示例中,一旦异步回流完成,如果我们有一个脚本调用了某个元素的 getBoundingClientRect () 方法,结果将从缓存中获取:

Render Tree 缓存失效

在实际应用中,大部分 Web 应用程序不是静态 HTML 网页。相反,它们使用客户端 JavaScript(如 React)来创建和更新可见元素。

当客户端 JavaScript 代码改变 DOM 时,它可以添加、移除或更新元素,这经常直接影响渲染树,从而导致先前缓存的样式和坐标失效。

当 JavaScript 向 DOM 中添加元素时,它们是没有样式和定位信息的。这是因为回流通常是异步执行的,会在绘制帧之前某个时刻在主线程上运行,此时 JavaScript 任务已经完成。

例如,考虑向 DOM 中 添加一个模态框。当模态框的 HTML 元素被插入 DOM 时,它们最初是未定位的:

 const modalRoot = document.createElement("div");
modalRoot.classList.add("modal--root");

const subDiv = document.createElement("div");
const paragraph = document.createElement("div");
// Add other DOM nodes and styles as needed...

document.body.firstChild.appendChild(modalRoot);
// DOM nodes are added!

这种变化使得渲染树的部分或全部缓存失效。随后,当异步回流发生时,每个 DOM 节点会被赋予样式和位置,然后显示在屏幕上:

当这个过程完成时,将更新并缓存渲染树:

通常浏览器不会完全重新计算整个树结构 —— 浏览器会尝试只重新计算受到影响的最小子集。因此,回流是增量的。

DOM 变动的大小和类型(例如添加单个元素、更新类、移除多个 DOM 节点等)不同,应用于渲染树的失效范围也会有所不同。

同步回流

我们已经讨论了 JavaScript 代码路径如何使渲染树失效,以及 JavaScript API 如何查询底层的布局引擎原语(例如getBoundingClientRect())。

结合这两个概念,让我们来展示一个使渲染树失效并强制进行回流的 JavaScript 代码片段:

 const element = document.getElementById("modal-container");

// 1. invalidate Layout Tree
element.classList.add("width-adjust");
// 2. force a synchronous reflow. This can be SLOW!
element.getBoundingClientRect();

在这里,我们首先执行更新一个 DOM 元素的类。这个操作使得布局树的一个子集失效,并标记该节点为脏节点。它的定位和样式信息不再准确,需要重新计算。

问题在于接下来的操作,即在失效的 DOM 节点上调用 getBoundingClientRect ()。这将触发同步回流,因为我们要求浏览器获取一个脏的 / 未定位的元素的位置。浏览器必须立即对其进行定位,以满足请求,否则它没有可用的准确信息。

从线程的角度来看,这将延长 JavaScript 任务的持续执行时间,可能导致一个长任务。

一旦同步回流完成,浏览器将在渲染树中缓存这些信息,以供后续访问使用。假设在渲染树没有其他更新的情况下,浏览器可能没有脏元素需要重新定位,因此异步回流可能会减少(甚至完全跳过!)。

强制回流并不总是表现为性能问题。在某些情况下,它只是将回流成本转移到 JavaScript 任务运行期间,而不是异步回流阶段(因为异步回流可以利用同步回流的缓存输出)。

不过,总的来说,如果可能的话,应该尽量避免无意中的同步回流。接下来我们将讨论为什么。

【第3228期】减少文件体积来优化性能,你的姿势对了吗?

布局抖动

如果在单个任务 / 帧内多次触发同步回流,所观察到的现象称为布局抖动(Layout Thrashing)。

比如下面的代码:

 const elements = [...document.querySelectorAll(".some-class")];

// In a loop, force a reflow for each element :(
for (const element of elements) {
element.classList.add("width-adjust"); // 1. invalidate Layout Tree
element.getBoundingClientRect(); // 2. force a synchronous reflow. This can be SLOW!
}

我们可以看到在同一个任务中多次使渲染树失效,然后强制进行回流。

根据失效的范围大小(例如,如果失效了渲染树的大部分),这可能会导致严重的性能问题!

在这种情况下,我们强制浏览器在单个帧中多次执行昂贵的同步回流操作:

不同于常规的同步回流,布局抖动不仅改变了回流的时机,还增加了回流操作的次数,这可能导致长任务(Long Tasks)的产生并降低帧率。

【第3275期】构建更快的 Web 体验 – 使用 postTask 调度器

避免布局抖动

在任何情况下,我们都要避免布局抖动。有一些通用策略可以预防它!

批量读取和写入

如果我们可以利用这两个事实,我们可以将上面引起布局抖动的代码改写为:

 const elements = [...document.querySelectorAll(".some-class")];

// Do all reads
const rects = elements.map((element) => element.getBoundingClientRect());

// Do all writes
elements.forEach((element) => element.classList.add("width-adjust"));

// Done! Asynchronous reflow will compute positions later.

这样做的好处是,昂贵的回流成本只会在异步回流期间发生一次。

要注意造成回流的各种浏览器 API
在开发 Web 应用程序时,您应该注意可能会强制触发回流的各种浏览器 API。虽然我们不需要不惜一切代价避免它们,但应在调用它们的时机上保持警惕,并进行适当的性能分析,以确保不会引发意外的同步回流。

React 开发

使用 React 并不意味着您免于布局抖动的影响。所有的 Web 应用程序都可能滥用浏览器的布局引擎,即使是使用现代 Web 框架编写的应用程序也是如此。

React 的声明式语法为实际 DOM 变动(和布局失效)的发生增加了一层间接性。这使得在代码中发现布局抖动变得更加困难。

根据我的经验,在 React 中,布局抖动通常是由于在 useEffect 中尝试测量 DOM 节点或其他 React 组件。常见的这类测量场景包括 Tooltip 固定位置提示或恢复滚动位置。

 function MyComponent() {
const elementRef = React.useRef();

// Be careful with Layout APIs in `useEffect`!
React.useEffect(() => {
// When does this run? Before or After DOM updates?
const rect = elementRef.getBoundingClientRect();

// do something with `rect`
}, []);

return <div ref={elementRef}>{/* more DOM nodes... */}</div>;
}

如果您不确定您的 React useEffect 回调是否会强制同步回流,您可以收集跟踪数据,并在火焰图中搜索强制回流的调用堆栈。

React 团队提供了一种专门的钩子,称为 useLayoutEffect,可以在 React 将其变更刷新到 DOM 后,读取位置信息:

 function MyComponent() {
const elementRef = React.useRef();

// Use the `useLayoutEffect` hook instead if you are forcing reflow.
React.useLayoutEffect(() => {
// This will run after React has flushed DOM updates.
const rect = elementRef.getBoundingClientRect();

// Use the values read from `rect`
// But writing to the DOM here will likely cause more reflow!
}, []);

return <div ref={elementRef}>{/* more DOM nodes... */}</div>;
}

总结

我们讨论了浏览器中回流的复杂生命周期,包括异步回流、同步回流和布局抖动。

作为 Web 开发者,我们必须注意浏览器何时以及如何使用其底层的样式和布局引擎,以确保 Web 应用程序充分利用浏览器提供的高度优化的增量设计,而不是逆其道而行。

这将确保我们提供一致且最佳的帧率和用户体验。

观点

  • 异步布局是浏览器性能优化的一部分,它允许在屏幕上渲染内容时,浏览器首先运行 JavaScript,然后计算样式,最后运行布局。

  • 强制同步布局是由于某些 JavaScript 代码(如 getBoundingClientRect ())的执行,它会立即触发浏览器执行布局,以便获取元素的样式信息和精确坐标。

  • 浏览器会尽可能地降低重排的成本,通过将每个可见元素的样式和位置存储和缓存到渲染树中,以便后续访问。

  • DOM 的变化,如添加、移除或更新元素,会导致先前缓存的样式和坐标失效,需要重新计算布局。

  • 布局抖动是指在单个任务 / 帧内多次触发同步回流,这会导致性能问题,因为它不仅改变了回流的时机,还增加了回流操作的次数。

  • 为了避免布局抖动,应该批量读取和写入操作,确保昂贵的回流成本只发生一次。

  • 在使用 React 开发时,虽然声明式语法增加了一层间接性,但仍需注意 useEffect 和 useLayoutEffect 钩子的使用,以避免不必要的同步回流。

  • Web 开发者应该注意浏览器何时以及如何使用其底层的样式和布局引擎,以确保 Web 应用程序充分利用浏览器提供的高度优化的增量设计。

关于本文
译者:@ikoofe
译文:https://mp.weixin.qq.com/s/_JRgxs5h8gA8MWvoyRct3w
作者:@Joe Liccini
原文:
https://webperf.tips/tip/layout-thrashing/

这期前端早读课
对你有帮助,帮”
“一下,
期待下一期,帮”
在看” 一下 。