前言
介绍了如何使用 CSS 的 content-visibility 属性来提高网页在渲染大量元素时的性能。今日前端早读课文章由 @ikoofe 翻译,公号:KooFE 前端团队授权分享。
正文从这开始~~
最近,我在 emoji-picker-element 上遇到了一个性能问题:在一个接近 20k 个自定义表情符号的 Fediverse 实例上,打开表情符号选择器时,页面至少冻结了一秒钟,之后会卡顿一段时间。
【第2073期】content-visibility: 一个可以提高渲染性能的css属性
与 Slack、Discord 等类似,在 Mastodon 或 Fediverse 的不同服务器上也可以有自己的自定义表情符号。对于 20k 大小的自定义表情符号虽然不常见,但也并非不会出现。
在启动了他们的 Repro 之后,它确实很慢:
这里存在的问题是:
20k 自定义表情符号意味着 40k 元素,因为每个元素都使用了
<button>
和<img>
由于没有使用虚拟化,这些元素全部都被塞进了 DOM 中
让人欣慰的是,在这里使用了 <img loading="lazy">
,所以那 20k 张图片并不是一次全部下载的。但无论如何,渲染 40k 元素都会非常缓慢 - Lighthouse 的建议是不要超过 1,400 个!
当然,我的第一个想法是,“谁会有 20k 的自定义表情符号?”,我的第二个想法是,“唉,我想我应该做虚拟化”。
当初,我刻意避免了 emoji-picker-element 中的虚拟化,即因为:1) 它很复杂,2) 我认为我不需要它,以及 3) 它会影响可访问性。
我之前有过做虚拟列表的经验:在另外一个类库 Pinafore 上实现一个很大的虚拟列表。其中,使用了 ARIA: feed role,我做了所有计算,并添加了一个选项来禁用 “无限滚动”,因为有些人不喜欢它。为了尽快解决这个卡顿问题,我开始打算实现类似的虚拟列表。
然而,几天后,我脑海中突然冒出一个想法:为什么不试试 CSS content-visibility 呢?从上面的性能监控截图中可以看到,在布局和绘画上花费了大量时间。因此,使用 content-visibility 可能是一个比全面虚拟化简单得多的解决方案。
content-visibility 是一个全新的 CSS 功能,它允许开发者从布局和绘制的角度 “隐藏” DOM 的某些部分。它在很大程度上不会影响可访问性(因为 DOM 节点仍然存在),它不会影响在页面中查找 (⌘+F/Ctrl+F),并且不需要虚拟化。它所需要的只是对屏幕外的元素进行调整,以便浏览器可以在其中保留空间。
在这里,可以把 emoji 的分类作为要调整的元素单元,比如 Fediverse 的自定义表情有 blobs、cats 等分类。
幸运的是,我有一个很好的 atomic 单元来调整大小:emoji 类别。Fediverse 上的自定义表情符号往往分为一口大小的类别:“blobs”、“cats” 等:
对于每个分类,表情符号的大小以及行数和列数是已知的,因此可以使用 CSS 自定义属性来计算出预期的大小:
.category {
content-visibility: auto;
contain-intrinsic-size:
/* width */
calc(var(--num-columns) * var(--total-emoji-size))
/* height */
calc(var(--num-rows) * var(--total-emoji-size));
}
这些占位符占用的空间与最终展现的空间完全相同,因此在滚动时不会有任何跳动。
接下来,我们来验证一下效果。对于初始加载,在 Chrome 中大约提升了 15%,在 Firefox 中提升了 5%。与虚拟列表相比,这个优化的效果并不能令人满意,因为虚拟列表可以做得更好!
我们继续看性能监控图。其中布局成本几乎没有了,但还有其他我无法解释的成本。例如,Chrome 跟踪中的这个大的无差别 blob 是怎么回事?
很显然, Chrome 对我们 “隐藏” 了一些性能信息时,我们可以这么做:取消 chrome:tracing,或者(最近)在 DevTools 中启用实验性的 “显示所有事件” 选项。这样 Chrome 会为我们提供更多的性能监控信息。设置好之后,截图如下:
这里出现了 ResourceFetcher::requestResource 提示信息。虽然我并不知道它是做什么的,但猜测是和 <img loading="lazy">
有关系。把所有 <img>
中的 src 中注释掉 - 这个神秘的成本就消失了!所以 loading="lazy" 也并不是免费午餐。这里最直接的做法是把 loading="lazy"
。
与其是去掉 loading="lazy",还不如直接把 <img>
移除掉,这样还能减少 DOM 元素属性,从 40k 个 DOM 元素减少到 20k。
我们可以使用 CSS 给 <button>
设置一个 ::after
伪元素,将图片作为它的背景来展示出来:
.onscreen .custom-emoji::after {
background-image: var(--custom-emoji-background);
}
此时,当视图滚动到某个分类时,只需通过 IntersectionObserver 来添加 .onscreen
。它的性能提升得更多。在基准测试中, Chrome 和 Firefox 中都实现了~45% 的改进,原始重现从~3 秒缩短到~1.3 秒。注意:由于跨浏览器的差异,Safari 的对 contentvisibilityautostatechange 的支持不够好,所以这里选择了 IntersectionObserver。
虽然,这个方案渲染 20k DOM 节点永远不会像虚拟化列表那样快。另外,一旦出现更多的表情符号,这个方案也无法扩展。但是 content-visibility 的实现成本比较低,我们根本不需要改变 ARIA 策略,也不用担心在页面中查找。但是如果追求更高的性能,虚拟列表仍然是最好的选择
关于本文
译者:@ikoofe
译文:https://mp.weixin.qq.com/s/iPFQEN2MgtzIfzXpW7J-cQ
作者:@Nolan
原文:https://nolanlawson.com/2024/09/18/improving-rendering-performance-with-css-content-visibility/
这期前端早读课
对你有帮助,帮” 赞 “一下,
期待下一期,帮” 在看” 一下 。