satgo1546’s ocean

Any sufficiently primitive magic is indistinguishable from technology.

content-visibility属性意在实现虚拟滚动

前天试图做一个树形数据表组件的时候破防了。面对大量数据,必须使用虚拟滚动。Android有RecyclerView,iOS有UICollectionView,都是上古老物了,可CSS呢?本该是UI技术最先进的平台,却连一个基础的构建基块都没有,第三方库提供的封装怎么用都别扭。又想到以前大战display: flow,还有永远对不齐的图标……Web就是这样的平台,简单的事很困难,困难的事不可能。

搜索虚拟滚动的时候发现了WICG虚拟滚动提案,不过已经寄了,继任者是WICG显示锁定提案,也已结束使命。在后者中迎面看到的就是熟悉又陌生的content-visibility和hidden=until-found,这几个属性都已经实装了。

我没想到:contain原来是一种虚拟滚动解决方案吗?

Note: content-visibility: auto can thus be used instead of complicated "virtual list" techniques, at least in many cases.
CSS Containment Module Level 2 § 4.2. Using content-visibility: auto

前情提要:在Web上隐藏元素有114514种方式。正常的就有display: nonevisibility: hiddenvisibility: collapseopacity: 0、HTML hidden属性,而异想天开的做法可以说是要多少有多少。所以当CSS中引入新的content-visibility属性时,我不以为然。但它解决的不是能力,而是性能。

因为CSS实在太自由了——任意子元素都可能顶出来或浮到外面,DOM的微小变更都可能导致全部重算,布局引擎很难在按部就班之外再做什么进一步优化。自由过头的代价就是另一方面的不自由。

由于缺乏其他降低性能影响的手段,传统的虚拟滚动方案需要真正卸载视口外的元素。通过visibility: hiddenopacity: 0隐藏的元素仍占着位置,占位的大小需要实际计算布局才能取得,并不能减少计算量。通过display: none隐藏的元素会被彻底从布局中移除,确实能减少计算量。但都做到这一步了,不如把元素删了。不管是display: none还是移除元素,带来的Ctrl+F查找不可用、焦点不可达等诸多可用性问题都是无法解决的,所以一般只在预见到数据量大的情况应用虚拟滚动。

contain系列和content-visibility属性就是为了打通网页开发者和布局引擎之间的隔阂,由网页开发者辅助信息来解决布局速度的问题。比如说,contain: paint乍看起来跟overflow: hidden没什么两样。可是overflow: hidden中的position: absolute元素仍能轻松夺框而出,所以布局引擎实际上仍必须完全处理内部的元素。contain: paint中的元素则确定无法逃离,因此只要容器离开了屏幕,其中的内容也就肯定不用再管了。

理论上,contain: strict已经提供了足够的保证,使浏览器可以无视屏幕外的此类元素,但因为只是一种推荐的优化,仍可能导致多余的渲染。此外,其隐含的contain: size导致必须手动指定元素宽高,而一般的网页设计需要依赖浏览器自动计算。所以,在还未追加content-visibility属性时,应用优化依然有困难。

content-visibility: auto提供了更贴近传统高级虚拟滚动解决方案的效果:尚未渲染的元素使用估测大小占位,进入视口的元素则会解除contain: size,正常渲染,精确计算大小;contain-intrinsic-size: auto 300px auto 150px还能缓存已计算过的元素大小;规范明确要求以上全部行为,而非仅仅提示。

现在,只要向列表项目上应用content-visibility: auto,就能在得到类似虚拟滚动的性能优化的同时,还保留Ctrl+F的可用性。此功能实装于Chrome 85、Firefox 125。


HTML Living Standard单页版是一个很适合的测试对象,这个页面是纯粹的字多,HTML源码有13MB之大。如果直接打开,在我的电脑上Chrome会满负荷运行10秒,期间DevTools卡死。但通过Stylus插件注入下列样式之后,则只会运行大约2秒,且DevTools始终可响应。

body > * {
	content-visibility: auto;
}

Firefox本来就很快,所以没有测试。我不确定结果是否是某种缓存或时机导致的波动,不知道有没有更准确的测试方法。