你以为页面卡只是因为JS太多?今天 CSS content-visibility 把这件事彻底变了
写过页面渲染性能优化的人都踩过这个坑——写了一堆 JS 优化、首屏加载优化,跑了 Profiler、压了 Bundle,LCP 下来了,结果用户一滚动列表页面还是卡。最后发现瓶颈根本不在 JS,在于浏览器在傻傻渲染那些根本不在屏幕上的内容。CSS content-visibility: auto 就是来解决这个问题的,一行 CSS 让浏览器跳过离屏内容的布局和绘制,真实项目能快 5-10 倍。
这个问题到底出在哪
浏览器渲染页面的标准流程是:解析 HTML → 生成 DOM → 计算样式 → 生成 CSSOM → 合并成渲染树 → 布局 → 绘制 → 合成。无论内容在不在屏幕上,这套流程都要完整跑一遍。
一个常见的商品列表页可能有 200 个卡片,其中前 10 个在首屏,后 190 个都在屏幕外。但浏览器初始化时会把全部 200 个卡片的布局和绘制都算一遍——这些计算对用户来说毫无意义,但实实在在占用了主线程时间。
传统的解法是虚拟滚动:只渲染屏幕内的 N 个节点,滚动时动态替换。但虚拟滚动需要 JS 维护 startIndex/endIndex、监听滚动事件、处理容器高度,一套下来 300 行代码起步,而且节点突然出现在 DOM 里会触发 CLS(页面抖动)。对于很多场景来说,成本太高。
content-visibility: auto 是什么
content-visibility 是 CSS Containment 规范里的一条属性,Chrome 85+/Firefox 125+/Safari 18+ 全支持,全球覆盖率超过 90%。它告诉浏览器:这段内容不在屏幕内的时候,可以跳过它的布局和绘制工作。
.card-list .card {
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}
就这么两行。浏览器会自动用 Intersection Observer 的机制,当元素滚动到视口附近时恢复渲染——整个过程不需要一行 JS。
contain-intrinsic-size: auto 300px 是必须配套写的。auto 关键字的意思是:记住元素上次渲染后的真实高度,下次出现时直接用,不需要重新算。如果没有这个,离屏元素的滚动条高度会出问题,页面会出现抖动(CLS)。
真实项目数据
有工程师在电商列表页实测(200 个商品卡片,首屏只显示 8 个):不加 content-visibility 时,首次完整渲染耗时 1.8 秒,加了一行 CSS content-visibility: auto 后,首次渲染降到 0.6 秒,提速 3 倍,滚动时掉帧基本消失。
内容密集型长文章站测试(100 篇文章卡片离屏):CSS 官方文档记载渲染耗时改进最高达 7 倍。某新闻站实测,FCP 从 1.9 秒降到 0.8 秒,TBT(总阻塞时间)从 580ms 降到 210ms。
content-visibility 和 loading=”lazy” 可以叠加使用:loading=”lazy” 节省网络请求,content-visibility: auto 节省渲染计算,双管齐下收益最大。
三种取值怎么选
content-visibility 有三个值,对应三种场景:
auto(最常用):离屏时跳过渲染,进屏时恢复渲染,保留布局占位。适合商品列表、博客文章列表、无限滚动 feed。这些场景绝大多数内容用户不会全部看完,离屏内容晚点渲染完全不影响体验。
hidden:完全不渲染,但保留布局状态(和 display: none 不同,hidden 不会销毁布局信息)。适合折叠面板、隐藏 Tab、侧边栏这类「暂时不需要显示」的内容。打开时不需要重建 DOM,响应极快。
visible:默认值,没有任何效果。就是提醒你「这段别用」。
/* 商品列表——离屏跳过渲染,进屏恢复 */
.product-card {
content-visibility: auto;
contain-intrinsic-size: auto 280px;
}
/* 折叠评论——完全不渲染,但保留状态 */
.comments-section {
content-visibility: hidden;
}
/* 首屏 Hero——必须立刻渲染,不要加 */
.hero {
/* 什么都不加,content-visibility: visible 是默认值 */
}
坑要提前踩
第一个坑:content-visibility: auto 会导致页面内搜索(Ctrl+F)搜不到离屏内容里的文字。这个对大多数内容站不是问题,但如果你的页面依赖 find-in-page 功能,需要注意。
第二个坑:离屏的 auto 元素对屏幕阅读器不可访问,直到渲染出来。这个基本不影响体验,但如果你做的是可访问性要求极高的产品,需要测试。
第三个坑:content-visibility: auto 的元素如果里面有 position: sticky 的子元素,离屏再进屏时 sticky 可能失效。需要实际测试你的页面有没有这个组合。
第四个坑:contain-intrinsic-size 必须写,不写会导致滚动条高度计算错误,页面抖动。这个是必须项,不是可选项。
content-visibility 和虚拟滚动怎么选
很多人会问:content-visibility 和虚拟滚动哪个好?答案是看场景。虚拟滚动解决的是「DOM 节点太多」(万级节点)的问题,节点卸载后根本不占内存。content-visibility 解决的是「渲染计算太重」(百千级节点)的问题,节点保留在 DOM 里只是跳过绘制。对于大多数几千个节点的列表页面,content-visibility: auto 比虚拟滚动简单得多,一行 CSS 搞定,不需要维护复杂的索引和滚动逻辑。对于真正的超长列表(两万节点以上),虚拟滚动仍然是正确的选择。
三步落地
第一步:找出你的「离屏大区块」。打开 Chrome DevTools → Performance 面板,录一次页面加载,找到 Render 阶段耗时最长的区块,那些就是目标。
第二步:给这些区块加 content-visibility: auto,配上 contain-intrinsic-size: auto xxxpx,然后重新跑 Performance 面板,对比布局和绘制阶段的时间变化。
第三步:检查你的页面有没有 Ctrl+F 强依赖、position: sticky 组合、屏幕阅读器高频使用场景,这三类情况需要额外测试。
浏览器不支持 content-visibility 时会静默忽略,页面正常显示,只是没有加速效果。这是标准的渐进增强,不需要写降级分支。
性能优化到了深水区,瓶颈往往不在你写的代码里,而在浏览器默认要做的那些「没用但费时」的工作上。content-visibility: auto 就是把这件事交给 CSS 处理,让 JS 回归它真正该管的事。
评论区
登录后可评论。