我在与 CSS 相关的性能方面有一些盲点。一个例子是 will-change
属性。这个名字很好。你告诉浏览器某个特定属性(或 scroll-position
或内容)将要发生变化。
.el {
will-change: opacity;
}
.el.additional-hard-to-know-state {
opacity: 0;
}
但这重要吗?我不知道。据我了解,它的作用是将 .el
转移到 GPU 上进行处理/渲染/绘制,而不是 CPU,这是一种速度提升。有点类似于经典的 transform: translate3d(0, 0, 0);
hack。在上面的确切情况下,它在我看来并不重要。在我脑海中,opacity
是“最便宜”的动画之一,因此 will-change
没有特别的好处。或者它可能在某些浏览器或设备上明显起作用,而在其他设备上则不起作用?毕竟这是前端开发。
在 2014/2015 年左右,有一段时间关于 will-change
的文章激增,这些文章警告说存在奇怪的行为,例如堆叠上下文中的意外变化,以及要小心不要“过度使用”它。还有一些建议传播开来,说你永远不应该在 CSS 样式表中直接使用此属性;你应该在状态更改之前仅在 JavaScript 中应用它,然后在你不再需要它后将其删除。
我不知道这些事情是否仍然适用。抱歉!我渴望阅读一篇关于 will-change
的 2022 年深度分析文章。我们有能力进行这种测试,所以我会把它放到 想法堆 中。但我的意思是,CSS 中有一些专门为了性能而设计的特性,让我感到困惑,我希望我能够更全面地理解它们,因为它们似乎非常重要。
以 Johan Isaksson 的 “如何用一行 CSS 代码将 Google 的数据网格滚动速度提高 10 倍” 为例。滚动性能提高 10 倍是一件大事!你知道他们是怎么修复的吗?
[…] 当我浏览“顶级链接网站”页面时,我注意到滚动存在明显的滞后。当选择显示更大的数据集(500 行)而不是默认的 10 个结果时,就会出现这种情况。
[…]
那么,我做了什么?我只是在Elements
面板上的<table>
上添加了一行 CSS 代码,指定它不会影响页面上其他元素的布局或样式。
table {
contain: strict;
}
contain
属性 是另一个我“有点”理解的属性,但我仍然认为它是我的盲点,因为我的大脑不会自动想到我什么时候可以用(或应该用)它。但这是一个问题,因为显然如果我能更好地理解 contain
,我构建的界面就不像现在这样高效。
还有一个!content-visibility
属性。我离理解它最近的一次是在观看 Jake 和 Surma 关于它的 视频 之后,他们使用它(以及 contain-intrinsic-size
和一些奇怪的魔数)来极大地加快长页面的速度。但我还没有完全理解的是,我什么时候应该在我的页面上使用它。
这三个功能都是“如果需要就使用”的功能吗?在发现某个东西(例如一个巨大的页面)的性能不佳之后,忽略它们直到发现问题,然后使用它们来尝试解决问题,这样做可以吗?几乎是“直到需要时才使用它们”,否则你就陷入了过早优化的境地。这样做的麻烦在于,除非你在性能最低的设备上积极测试,否则你实际上不会注意到性能不佳。
或者这些功能是“这就是现代 CSS,你应该像对待 padding
一样对待它们”的领域?我有点怀疑情况更像是这样。如果你正在构建一个你知道在某些方面不会改变的元素,那么可能值得“包含”它。如果你正在构建一个你知道在某些方面会改变的元素,那么可能值得向浏览器提供这些信息。如果你正在构建页面的一部分,你知道它始终在页面的下方,那么可能值得避免绘制它。但就我个人而言,我还没有完全理解它,所以无法提供任何可靠的建议。
也许 DevTools 性能或 Lighthouse 报告可以建议使用这些属性?浏览器比开发者更了解 DOM 和渲染树,因此它可以更好地识别哪个元素会导致性能问题以及问题的类型,然后建议使用适当的 CSS 功能来缓解问题。
我认为,如果你从 CPU/GPU 的角度考虑
will-change
,那么你走错了路。更好地理解它的方法是认识到渲染的网页可以被分成几个层。每个层独立地绘制成静态图像(当它发生变化时需要重新绘制整个图像),然后将这些图像组合在一起,一个在另一个之上。造成性能差异的是三个问题
有多少层?层数越多,需要的合成操作就越多。但是,如果层数不够,则更改可能会发生在一个层内,并且需要更频繁地重新绘制该层。
每个层的尺寸是多少?层越小,合成过程中需要计算的像素就越少。
必须使用哪种合成算法?如果合成操作是
over
,并且上层是不透明的,则旧像素将被新像素覆盖,不需要计算,但如果涉及一些透明度,甚至混合算法,则每个像素都是一个计算周期的结果。浏览器会自行找出将页面最佳地划分为层的方案。你提到的属性有助于确定它们。如果设置它们有助于回答上述问题,请尝试使用它们。
我从 Google 的 Martin Splitt 在 2017 年发表的演讲中了解到这一点,该演讲是用 德语 和 英语 发表的。
Chris,我是你博客的长期读者,我想说我非常喜欢你对你不完全理解的事情的谦逊、坦诚的写作风格。谢谢!!
是的。这是优化黄金法则:分析、分析、再分析。然后使用这些属性来解决你在分析中发现的问题。例如,如果你在分析中发现屏幕上有大量元素时,Recalculate Style 时间很长,则使用 content: strict。
你并不一定需要在慢速设备上测试,因为即使绝对处理时间很短,分析也可以揭示相对时间比例是否看起来不对。
我喜欢 ccprog 的评论。不同的层或段具有不同程度的改变,以及我们如何提供提示,让底层机制(用于渲染、绘制、覆盖等)作为最佳性能的辅助,这很有趣。作为一个老兵,它让我想起了过去,当时我们显式地将软件划分为逻辑段,并定义了“叠加地图”,以便操作系统知道哪些部分需要同时驻留在内存中以实现最佳性能,以及哪些部分在完成后可以被交换出去,所有这一切在虚拟内存/分页系统出现后都变得过时了——跟踪哪些页面被频繁访问,需要保存在真实内存中以实现性能。
即使使用
will-change
,也可能会发生重新布局和重新绘制。似乎浏览器无法保证任何事情,因为许多其他参数可能会阻止优化。我认为
will-change
更像是对浏览器的建议,而不是实际的指令。我认为
will-change
和contains
属于两个不同的类别*
will-change
可能会导致浏览器执行一些它本来不会自己执行的额外工作*
contains
告诉浏览器,它不需要执行一些它通常会执行的工作(绘制或布局)因此,在我看来,
contains
更加安全……但确实,应该避免过早优化。合成通常比重新绘制更快。绘制是破坏性的,而合成不是。在 Photoshop 拥有可编辑的文本之前,你需要输入你想要的文本,它会被栅格化为当前层。如果你想将其移动到不同的位置或将其放大或改变其不透明度,你就必须重新绘制栅格化的文本,并重新使用文本工具。或者,你可以在透明层上使用文本工具,这样就可以重新定位它,而无需从背景中擦除它。
如今,浏览器在需要时(例如对于 `opacity` 和 `transform`)可以很好地自动创建合成图层。但如果这种情况突然发生,例如响应用户事件,则需要完成大量工作:在没有该元素的情况下重新绘制背景,分配新的合成图层并将元素绘制到该图层上,将两个绘制的图层移动到 GPU,然后合成它们。在过渡开始时完成所有这些操作会导致一些卡顿,因为这些像素在最初的几帧中被来回移动。这就是 `will-change` 发挥作用的地方。它可以让你建议浏览器现在完成这些步骤,以备将来动画、过渡或以其他方式更改合成器可以轻松处理的属性(主要是 `opacity` 和 `transform`)。因此,当你能够在需要更改合成属性之前建议浏览器进行准备时,`will-change` 最有优势。否则,浏览器会按需进行处理。
然而,正如 ccprog 指出,你不应该不必要地添加 `will-change`。创建太多合成图层会占用大量的视频内存,并且将绘制更改推送到图形卡上的这些图层也会带来开销。它不是万能药,也不能保证性能提升。它有可能对性能产生负面影响。
除了分析之外,使用开发工具中的“绘制闪烁”和“图层边框”渲染选项可以帮助你了解浏览器正在做什么,或者 `will-change` 和 `contains` 如何影响性能。