长列表卡了八年,你以为只能靠装虚拟滚动库救场?今天两行 CSS 让浏览器自己跳过了不需要渲染的内容
长列表页卡了八年,你以为只能靠虚拟滚动库救场?今天两行 CSS 让浏览器自己跳过了不需要渲染的内容。
这件事的根子出在浏览器的渲染逻辑上:不管内容在不在视口里,浏览器都会把它整个算一遍。写一个三千个商品卡片的列表,哪怕用户只看得见前五个,浏览器也得把三千个全部 layout 加 paint 完才肯罢休。CPU 占用率高、FCP 慢、内存吃紧——这不是代码写得不好,是平台底层的设计假设和长内容场景不匹配。
以前能做的有限:要么装一个几十 KB 的虚拟滚动库,运行时只渲染视口内那几条;要么用 IntersectionObserver 配合 JS 控制显隐,本质上还是在页面上造假。IntersectionObserver 只是观察什么时候进视口,真正跳不跳渲染还是靠开发者自己写逻辑,两个 API 配合起来才能近似达到目的。
content-visibility: auto 解决了这个十年老问题。它是 CSS 规范里的原生属性,告诉浏览器:「视口外的内容先别渲染,等它快出现了再说。」浏览器会跳过那些离屏元素的 layout、style 和 paint,只保留 DOM 节点和占位高度——页面渲染的任务量直接从全量降到只有可见区域那一小部分。
关键的坑:contain-intrinsic-size 必须同时写。content-visibility: auto 让浏览器跳过渲染,但跳过的元素高度默认是 0,滚动条会把它算成零高度,整个滚动区域的高度就乱掉了,用户一滚就看到滚动条跳来跳去。contain-intrinsic-size 就是在告诉浏览器:「这块内容预估占这么多高度,先按这个算。」Chrome 明确建议配对使用,少写任何一个都会让体验变差。
.card {
content-visibility: auto;
contain-intrinsic-size: auto 280px;
}
contain-intrinsic-size 的 auto 关键字尤其重要:卡片第一次滚入视口完成渲染后,浏览器会记住它的真实高度;再滚出去又滚回来,直接复用记录值,不再用写死的 280px 估算,避免滚动位置每次都抖一下。
性能数据是实打实测出来的。CSDN 上一位开发者拿三千个含阴影、圆角、标签、按钮的复杂商品卡片做了对照实验(4x CPU Throttling):未启用时首屏渲染总耗时 2840ms,初次 layout 1420ms,paint 960ms,堆内存占用 86MB;加上 content-visibility: auto 后分别压到 420ms、85ms、60ms、22MB——layout 减少 94%,paint 减少 93%,内存下降 74%。Google 官方演示的长文章场景里,渲染耗时从 232ms 掉到 30ms,提速接近七倍。
content-visibility 有两个值,auto 和 hidden,用法完全不同:auto 是懒渲染,离屏跳过、靠近时再画,适合商品列表、文章 feed、评论区这类「大部分时候不看但随时可能看」的内容;hidden 则是彻底不渲染,类似 display: none 但保留布局占位,适合还没点开的折叠面板、手风琴这类「不出现就不需要任何渲染资源」的场景。
一个常见的误区是把 content-visibility 当成虚拟滚动的替代品。DOM 节点还在,只是跳过了 paint 和 layout 而已。如果列表有三万个节点,虚拟滚动会把不在视口内的节点彻底从 DOM 里移除,内存占用是 O(1);content-visibility 只是 O(OFFSCREEN)——离屏的元素依然在 DOM 树里,只是不画出来。万级节点以上的真·虚拟列表场景,虚拟滚动库依然不可替代;但对于中等规模的长页面(几十到几百个复杂卡片),content-visibility 用一行 CSS 就能拿到 80% 的收益,而且零依赖。
浏览器兼容性不需要担心:Chrome 85+、Firefox 125+、Safari 18+(含 iOS Safari 18+)全部支持,全球覆盖率超过 90%。不支持的浏览器会直接忽略这个属性,按原有逻辑全量渲染,没有功能破坏——是典型的渐进增强场景。
具体用在哪里:商品卡片列表、文章预览 feed、评论区、相关推荐区块、文档页的各个章节。首屏可见的 Banner 和关键内容不要加,加了反而会延迟首屏渲染。
下一步:打开 Chrome DevTools 的 Performance 面板,录制一次页面加载,看 Layout/Paint 阶段耗时最长的节点在哪个区域——大概率就是离屏列表或评论区。两行 CSS 改完再录一次,差值就是你能拿到的实际收益。
评论区
登录后可评论。