DOM 性能案例研究
回流
批处理
各种各样的
从不同角度
关于虚拟 DOM
底线
资源
这篇文章取自我的博客,所以一定要查看它以获取更多最新内容😉
我有一个有趣的问题要问你——你上次使用真正纯粹的DOM API和方法来构建真实项目是什么时候?是的,我也记不清是什么时候了。😂 但它们真的存在过吗?因为,你知道,你几乎总是在 DOM API 的帮助下使用 HTML 来做一些更具交互性的事情,但你绝对不会将它用作创建 UI 的独立方式。但是,随着现代UI 框架和库(如React、Vue或Angular)的出现,时代变了,创建 UI 的方式也变了。所以,除非你使用一些将代码编译为 HTML/CSS/JS 三重奏的框架,否则你最有可能将你的应用程序基于一些基于 DOM API 的工具。😉 话虽如此,这些工具为我们提供的控制级别是令人难以置信的。它确实有助于创造更好、更漂亮、更快的体验。是的,速度——这就是我们今天要研究的。
你可能知道或听说过,任何与 DOM 的交互都是代价高昂的。如果使用不当,这些调用可能会严重影响性能。即使我们谈论的是几分之一毫秒,它仍然很重要。如果你的 UI 无法流畅运行,锁定在60 FPS(+1/-1)帧率,那么肯定有问题。但你的 Vue/React/Angular 应用不应该出现这种情况,除非你做了一些非常糟糕的事情或执行了高要求的任务(BTC 挖矿、WebGL、AI 和其他数字运算工作😁)。这是因为这些工具的优化程度很高。所以,让我们在这里做一个案例研究,并检查一些DOM 优化技术,包括这些库所使用的技术,以了解它是如何完成的!尽情享受吧! 👍
回流
从最臭名昭著的开始,那就是重排- 它既是你最大的敌人,也是你最好的朋友。重排(也称为布局破坏🗑)是指当你与 DOM、CSS 和所有那些东西交互时,浏览器中发生的所有过程的名称。这意味着重新渲染和重新计算你的网站的布局(元素的位置和大小)。这一切都很好 - 重排在后台处理所有这些复杂的问题。让我们继续讨论更糟糕的部分 - 重排是一个阻塞用户的操作!这意味着如果在执行重排时有太多工作要做,你的 UI 可能会降低其帧速率、冻结,或者在最坏的情况下甚至崩溃。这些都是你可能不希望用户拥有的体验。话虽如此,处理 DOM 并因此导致重排时要格外小心是很重要的。
那么,究竟是什么触发了回流呢?如果你想了解更多,GitHub 上的 gist列表非常详细。这里我们先快速看一下其中最重要的几点:
getComputedStyle()
- 极其有用且极其昂贵;- 框度量和滚动-诸如此类
clientHeight
;scrollTop
- 窗口属性-
clientHeight
,scrollY
; - 事件位置数据和SVG
这些只是一些基础的、更通用的方法。当然,某些任务(例如访问属性)的性能开销(回流时间)比某些更高级的方法(例如)要小getComputedStyle()
。
批处理
那么,重排真的很糟糕。我们可以做些什么来尽量减少重排,或者至少优化它们以提升性能呢?🚀 嗯,实际上有很多方法。首先,最好也是最流行的技术是批处理。它的基本含义是,你应该将DOM 的读写操作分组,并尽可能分别提交。这个过程允许浏览器在底层优化你的调用,从而整体提升性能。
// This will always be faster...
const width = element.clientWidth + 10;
const width2 = element.clientWidth + 20;
element.style.width = width + 'px';
element.style.width = width2 + 'px';
// ...than this.
const width = element.clientWidth + 10;
element.style.width = width + 'px';
const width2 = element.clientWidth + 10;
element.style.width = width2 + 'px';
除此之外,您还应该批量处理并减少任何其他类型的 DOM 交互。例如,让我们以向 DOM 树添加新元素的标准方式为例。当您只添加一两个新元素时,可能不值得这么麻烦。但是,当我们讨论数十个或数百个元素时,正确提交此类调用就非常重要了。我的意思是什么呢?嗯,就是将所有这些调用批量合并为一个,最有可能的方法是借助DocumentFragment
。
// Unoptimized
for(let i = 0; i < 100; i++){
const element = document.createElement('div');
document.body.appendChild(element);
}
// Optimized
const fragment = document.createDocumentFragment();
for(let i = 0; i < 100; i++){
const element = document.createElement('div');
fragment.appendChild(element);
}
document.body.appendChild(fragment);
如此简单的改变就能带来巨大的改变。我认为,毋庸置疑,你应该随时随地应用相同的做法/理念。除此之外,浏览器的开发工具也非常有用。你可以使用它的渲染时间轴来查看所有关于 DOM 渲染方式的相关数据。当然,只有在你进行适当的优化后,它才会有用。
各种各样的
现在,我们来谈谈更一般的问题。最显而易见的建议就是保持简单。但深入来说,这到底意味着什么呢?
- 减少 DOM 深度- 不必要的复杂性只会让速度变慢。此外,在很多情况下,当你更新父节点时,子节点也可能需要更新,从而导致需要处理指定节点下形成的整个结构。更新还可能引发 DOM 树上层的变更。简而言之,这会增加回流的时间。
- 优化 CSS - 当然,那些未使用的 CSS 规则实际上根本不需要。您应该删除其中任何规则。此外,复杂的CSS 选择器也可能导致问题。但是,如果您已经遵循了上一条规则,那么这些规则可能就毫无用处了,您的代码中根本不需要它们。内联经常更改的样式也是一种很好的做法。显然,相比之下,被多个元素使用的样式应该单独创建为CSS 规则。
- 动画- 这些动画可能会造成很大的影响。你应该尽可能将动画限制在 transform 和 opacity 属性上。此外,最好将它们包含在流外,也就是说,将 设置
position
为absolute
或fixed
。这可以确保你的动画不会干扰 UI 的其余部分,从而导致更慢的回流。此外,通过 属性,让你的浏览器知道指定的属性将会发生改变will-change
。最后,你可能想使用CSS 动画或Web Animations API来制作动画。这样,所有动画都会在单独的特殊“合成线程”中执行,从而使它们成为非阻塞的。
这些技巧可以大幅提升你的表现!所以,请随时使用它们。
从不同角度
现在我们知道,为我们处理视图更新的重排是万恶之源😈,让我们总结一下,并从稍微不同的角度来看待所有以前的信息。
屏幕上发生的一切都应该保持人人梦寐以求的60 FPS。这意味着屏幕每秒应该刷新 60 次(对于刷新率更高的设备,刷新次数应更高)。更具体地说,单帧上发生的所有操作(JS、回流等)都应该在10 毫秒以内完成(实际上大约有 16 毫秒,但浏览器会将这 6 毫秒用于内部事务)。话虽如此,当任务过大且耗时过长(超过 10 毫秒)时,帧率就会下降,并出现卡顿。
让我们看一下这个图表,看看这一帧上到底发生了什么:
我认为JavaScript部分不需要进一步解释,除了它通常会触发视觉变化(它也可以是 CSS 动画、Web 动画 API 等)。
Style标记了样式计算发生的时间。所有 CSS 规则(CSS 选择器等)都在这里被处理和应用。
布局和绘制步骤对我们来说至关重要,因为它们可以轻松优化。布局步骤是重排的起点。在上一步应用样式之后,需要处理可能需要重新计算几何形状的width
属性。这包括、height
、等。这些属性的更改可能需要更新left
其他元素,包括 DOM 树的上层和下层元素。top
优化此步骤的方法是,明智地管理这些属性的更改,或者拥有良好的 DOM 层次结构,以便在一次元素更新时无需进行太多更改。当然,您也可以更改position
属性。超出正常流的元素不会触发其他元素的更改。当没有布局属性更改时,浏览器会省略此步骤。
之后是绘制步骤。这里处理不影响布局的属性。这些属性包括background
、color
和shadow
类似的。通常情况下,重绘是纯粹的视觉效果。重绘的开销不如布局更改那么大,并且(和以前一样)在不需要时会被省略。
合成是最后一步,也是必不可少。所有之前创建的图层都会被粘合在一起,形成最终效果。之后,这些效果会逐像素地绘制到屏幕上。
我认为这些关于这一切背后的原理,真的可以激励你进一步深入研究如何优化你的代码。此外,如果你认为你的应用程序无需任何优化就足够快,想想看,有了这些额外的计算能力,你可以做什么——更丰富的视觉效果、更出色的动画——选择简直是无穷无尽的!🌟
关于虚拟 DOM
了解了这些技巧和窍门之后,我想你现在应该能轻松理解虚拟 DOM背后的神奇之处了。虚拟DOM 最近如此流行,主要得益于React和Vue的巨大影响力。它允许你将可视化节点的数据保存在 JS 原生结构中,从而无需访问 DOM(从而避免重排等问题)!
那么,简而言之,它是如何工作的呢?首先,你需要与 VDOM 交互并将更改应用到其中。然后(我可能忽略了一些更详细的内容,但这非常重要😁)是协调步骤。在这里,将新的 VDOM 树与旧的 VDOM 树进行比较,以区分更改。这些更改稍后会应用到真实的 DOM 中。
现在,协调步骤才是诸如 React 与 Vue(性能方面)之争的真正起源。这种比较实际上是许多人所熟知的虚拟 DOM 背后最重要、最关键的理念。React 16(React Fibre)在这方面做了非常出色的优化。但 Vue 同样令人印象深刻,它的虚拟 DOM 实现能够选择性地选择需要更新的节点(而不是像 React 那样——向下更新整棵树)。总之,这两个框架在提升众多 JS 程序员的性能和开发体验方面做得非常出色,所以为此点赞!👍
底线
我希望本文能让你了解如何提升基于 JS/React/Vue/DOM 应用的性能。😀 所以,我想,除了让网络速度更快之外,没什么可说的了。😄 一如既往,如果你喜欢这篇文章,可以考虑查看并分享我的个人博客。此外,请在Twitter和Facebook上关注我,获取更多精彩内容。🚀
资源
如果你想了解更多关于本文讨论的主题,从现在开始,每篇文章都能找到。😉 一切都是为了更好的阅读体验!✌
- 最小化来自developer.google.com的浏览器回流
- 什么强制布局/重排来自gist.github.com
- 来自developer.google.com的渲染性能
- 来自developer.google.com的动画和性能
- React Fibre 架构来自github.com
- 来自vuejs.org的Vue 比较