给浏览器画个圈:CSS contain 如何让页面从“卡成PPT”变“丝滑如德芙”
引言“这个页面滚动怎么像在泥潭里走路”去年双十一前夕我们团队接到了一个紧急优化任务商品详情页在低端机上滚动卡顿帧率掉到 20 以下用户投诉满天飞。我打开 Performance 面板发现每次滚动浏览器都在疯狂地计算一个离屏广告位的布局和样式。那个广告位在页面最底部用户可能根本不会翻到那里但浏览器依然在每一帧都兢兢业业地重新计算它的位置、大小甚至重绘它的阴影和圆角。我当时对着屏幕大喊“大哥你算它干嘛它又不在屏幕上”后来我才知道原来 CSS 里有两个低调但强大的属性可以告诉浏览器“这个元素你别管了它的内部变化不会影响外面你爱咋咋地。”它们就是contain和content-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 实战给离屏广告加结界回到开头那个离屏广告位.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后初始渲染时只会渲染前几条在视口内和附近的其余的直接跳过渲染时间从几百毫秒降到几十毫秒。.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 秒滚动掉帧严重。我们做了两步给每个卡片加上content-visibility: auto。配合contain-intrinsic-size: 0 120px给卡片一个预估高度。结果首屏渲染时间降到 300ms滚动流畅度接近原生。用户不知道我们在背后做了手脚只知道“这个页面好快”。五、注意事项别乱用否则会“翻车”5.1 兼容性contain所有现代浏览器都支持包括 Safari 15.4IE 不支持。content-visibilityChrome/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滚动条会随着你滚动不断跳动体验极差。务必给一个合理的占位高度。六、总结学会给浏览器“减负”浏览器很努力但它的努力有时候是徒劳的。作为开发者我们应该用contain和content-visibility告诉它“这些元素不用管那些元素等用户看到了再管。”这两个属性就像给页面装上了“节能模式”让性能提升肉眼可见。下次当你遇到长列表或复杂离屏组件卡顿时不妨试试它们。最后留一道思考题如果一个元素既设置了content-visibility: auto又设置了overflow: auto内部滚动条的行为会有什么不同为什么每日一问你在项目里用过content-visibility吗有没有遇到过奇怪的 bug评论区聊聊你的“偷懒”经验