给浏览器画个圈:CSS contain 如何让页面从“卡成PPT”变“丝滑如德芙”

引言

"这个页面滚动怎么像在泥潭里走路?"

去年双十一前夕,我们团队接到了一个紧急优化任务:商品详情页在低端机上滚动卡顿,帧率掉到 20 以下,用户投诉满天飞。

我打开 Performance 面板,发现每次滚动,浏览器都在疯狂地计算一个离屏广告位的布局和样式。那个广告位在页面最底部,用户可能根本不会翻到那里,但浏览器依然在每一帧都兢兢业业地重新计算它的位置、大小,甚至重绘它的阴影和圆角。

我当时对着屏幕大喊:"大哥,你算它干嘛?它又不在屏幕上!"

后来我才知道,原来 CSS 里有两个低调但强大的属性,可以告诉浏览器:"这个元素你别管了,它的内部变化不会影响外面,你爱咋咋地。"它们就是 containcontent-visibility

今天,我们就来认识一下这两位"性能救星"。

一、问题:浏览器为什么要"管闲事"?

在渲染引擎的眼里,页面上的每一个元素都可能与其他元素产生"瓜葛":

  • 一个元素尺寸变化,可能导致父元素、兄弟元素甚至整个文档流都重新布局(回流)。
  • 一个元素背景色变化,可能触发它所在层的重绘。
  • 即使元素在屏幕外,浏览器也不知道它将来会不会突然出现在视野里,所以依然会参与全局的布局和绘制计算

这种"过度负责"的精神,在元素数量爆炸的今天,成了性能杀手。

二、contain:给元素画一个"结界"

contain 属性允许你声明一个元素独立于文档的其余部分,它的内部变化不会"越界"影响到外部。这就像给元素画了一个魔法结界,结界里面怎么折腾,外面都不受影响。

2.1 contain 的几个"法术"

你可以给 contain 指定一个或多个值,告诉浏览器该限制哪些方面:

作用 效果
layout 限制布局 内部元素的布局不会影响外部,外部也不会影响内部。浏览器可以独立处理该元素的布局。
paint 限制绘制 内部元素永远不会超出元素边界绘制(相当于 overflow: hidden 但更强),且内部的重绘不会扩散到外部。
size 限制尺寸 元素的尺寸不依赖其子元素。你必须手动设置宽高,否则尺寸会按 0×0 计算。这能让浏览器跳过子元素的布局计算来确认父元素尺寸。
style 限制样式 内部元素的计数器、@counter-style 等不会影响外部。这个值较少用。
strict 等同于 layout paint size 最严格的隔离。
content 等同于 layout paint 常用组合,相当于隔离布局和绘制,但不隔离尺寸。

2.2 实战:给离屏广告加结界

回到开头那个离屏广告位:

css 复制代码
.ad-banner {
  contain: layout paint; /* 广告位内部布局和绘制不会影响到页面其他部分 */
}

加上这行 CSS 后,浏览器在计算滚动时的布局时,会把这个广告位看作一个"黑盒"。只要它的整体位置没变(比如固定在底部),内部的任何变化都不会触发外部重排。滚动帧率瞬间从 20 飙到 60。

注意 :如果使用了 contain: size,你必须给元素明确设置宽高,否则它会被压缩成 0×0,里面的内容可能看不见。一般用 contain: layout paint 就足够了。

三、content-visibility:让浏览器"偷懒"更彻底

如果说 contain 是"结界",那 content-visibility 就是"隐身术"。它直接告诉浏览器:这个元素如果不在屏幕上,你就别渲染它内部的东西

3.1 三个值

  • visible:默认行为,正常渲染。
  • hidden:元素不可见,且不占位(类似 display: none),但内部内容不会被渲染。
  • auto智能模式,当元素接近视口时开始渲染,离开视口时停止渲染。这是最常用的值。

3.2 效果有多夸张?

假设你有一个包含 1000 条评论的长列表,每条评论结构复杂。用 content-visibility: auto 后,初始渲染时只会渲染前几条(在视口内和附近的),其余的直接跳过,渲染时间从几百毫秒降到几十毫秒。

css 复制代码
.comment-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 100px; /* 给一个预估高度,避免滚动条跳动 */
}

contain-intrinsic-size 是为了在元素未渲染时给浏览器一个占位尺寸,防止滚动条忽大忽小。

3.3 与 contain 的关系

content-visibility: auto自动应用 contain: layout paint style (即 content 组合)。所以它本身就是一种强隔离 + 按需渲染。

四、真实案例:让一个"万人直播列表"起死回生

曾经有个需求:页面左侧是一个实时更新的万人直播列表,每个直播间卡片都有封面图、标题、观众数、礼物动画等。不加优化时,页面初始渲染要 2 秒,滚动掉帧严重。

我们做了两步:

  1. 给每个卡片加上 content-visibility: auto
  2. 配合 contain-intrinsic-size: 0 120px 给卡片一个预估高度。

结果:首屏渲染时间降到 300ms,滚动流畅度接近原生。用户不知道我们在背后做了手脚,只知道"这个页面好快"。

五、注意事项:别乱用,否则会"翻车"

5.1 兼容性

  • contain:所有现代浏览器都支持(包括 Safari 15.4+),IE 不支持。
  • content-visibility:Chrome/Edge 85+、Firefox 90+、Safari 15.4+ 支持。对旧浏览器可以用 JavaScript 降级或直接忽略(不影响功能)。

5.2 小心"过度隔离"

如果一个元素内部有弹出层(比如下拉菜单),并且这个弹出层使用了 position: absolute 定位到元素外部,contain 可能会限制它超出边界(因为 paint 会裁剪超出部分)。这时你需要避免使用 paint,或者把弹出层移到 contain 容器外部。

5.3 不要滥用 content-visibility

给所有元素都加上 auto 并不明智。它主要用于长列表、离屏组件等。对于已经在视口内的小组件,反而可能增加额外开销。

5.4 别忘了预估尺寸

如果没有 contain-intrinsic-sizecontent-visibility: auto 的元素在未渲染时高度为 0,滚动条会随着你滚动不断跳动,体验极差。务必给一个合理的占位高度。

六、总结:学会给浏览器"减负"

浏览器很努力,但它的努力有时候是徒劳的。作为开发者,我们应该用 containcontent-visibility 告诉它:"这些元素不用管,那些元素等用户看到了再管。"

这两个属性,就像给页面装上了"节能模式",让性能提升肉眼可见。下次当你遇到长列表或复杂离屏组件卡顿时,不妨试试它们。

最后留一道思考题 :如果一个元素既设置了 content-visibility: auto,又设置了 overflow: auto,内部滚动条的行为会有什么不同?为什么?


每日一问 :你在项目里用过 content-visibility 吗?有没有遇到过奇怪的 bug?评论区聊聊你的"偷懒"经验!

相关推荐
英俊潇洒美少年2 小时前
react19和vue3的优缺点 对比
前端·javascript·vue.js·react.js
维度攻城狮2 小时前
Docker-Ubuntu安装并启动Chrome浏览器
chrome·ubuntu·docker·安装
娇娇yyyyyy2 小时前
QT编程(18): Qt QItemSelectionModel介绍
开发语言·qt
豆豆的java之旅2 小时前
软考中级软件设计师 数据结构详细知识点(含真题+练习题,可直接复习)
java·开发语言·数据结构
sthnyph2 小时前
QT开发:事件循环与处理机制的概念和流程概括性总结
开发语言·qt
大尚来也3 小时前
Java 反射:从“动态魔法”到生产实战的避坑指南
开发语言
无心水3 小时前
Java时间处理封神篇:java.time全解析
java·开发语言·python·架构·localdate·java.time·java时间处理
m0_587958954 小时前
C++中的命令模式变体
开发语言·c++·算法