使用 content-visibility 和 contain-intrinsic-size 跳过屏幕外渲染
了解内容可见性功能如何通过自动裁剪来降低长页面的服务器渲染成本,为何必须使用“包含内在尺寸”选项,以及它与虚拟化技术的区别。
那些包含大量重复内容的页面,比如产品列表、动态内容流、存档信息以及大型表格,即便其 JavaScript 文件体积很小,往往也会显得加载缓慢。原因通常在于浏览器需要为页面上的每个元素计算样式和布局,包括那些用户尚未滚动到且可能永远无法看到的数百个卡片。本指南将介绍如何利用两个 CSS 属性 content-visibility 和 contain-intrinsic-size 让浏览器推迟这些计算工作,说明这一改动与核心网页指标及搜索引擎索引的关系,以及该技术何时会失效或不适用。
典型案例:无明显原因却加载缓慢的长列表
想象一下某个在线商店的“查看所有产品”分类页面。大约600张产品卡片,每张都包含图片、标题、价格和评分,会在服务器上被整合成一页长页面。该页面没有分页功能、无限滚动功能,也没有虚拟化技术。商家喜欢这种方式:一个URL即可访问所有产品,且可以通过浏览器的页面查找功能定位到任何内容。
在一款中端笔记本电脑上,这个页面需要接近四秒的时间才能具备交互性。人们首先会怀疑是JavaScript的问题:过大的代码包、行为异常的useEffect函数,或是组件在循环中不断重新渲染。但仔细检查代码包后并未发现导致延迟的原因。
DevTools中的性能面板则揭示了不同的情况。主线程的大部分时间都用于布局操作,而这一过程早在任何脚本发挥作用之前就已经发生。
为何屏幕外的内容仍会带来成本
渲染并非单一操作。浏览器首先为每个元素解析样式,接着进行布局计算以确定每个元素的尺寸和位置,最后才绘制像素。虽然像素绘制主要局限于视口内或附近的区域,但样式解析和布局计算会覆盖整个文档,包括远在视口下方的内容。等到浏览器决定要绘制什么内容时,那些耗时的几何计算早已完成。
在商店示例中,这意味着在购物者看到第一件商品之前,所有600张卡片——每张都包含图片框、标题、价格以及评分行——都必须先经过样式和布局处理。购物者通常只能看到大约8件商品,其余592件目前还无用处,但浏览器无法判断访问者是否会向下滚动,因此默认会将它们全部视为需要立即显示的内容。这正是应当消除的浪费。如果您想了解这些处理阶段的成本差异,可参阅我们关于重排、重绘和合成分别会给浏览器带来多少成本的解析。
解决方案:在重复元素上添加两次声明
将这两个属性应用到会重复出现的元素上,此处即为产品卡片。第一个属性告知浏览器:当卡片远离视口时,可以跳过对其内容的渲染。第二个属性则为那些实际尺寸尚不确定的卡片提供占位尺寸。
.product-card {
content-visibility: auto;
contain-intrinsic-size: auto 340px;
}
使用 content-visibility: auto 时,远离视口的卡片仍会保留在 DOM 及无障碍访问树中,find-in-page 功能依然能够定位到其文本。不同的是,在卡片接近屏幕之前,浏览器不会对其内容进行样式处理、布局计算或绘制操作。实际上该属性会对该元素应用布局、样式和绘制的限制机制,从而使浏览器能够将该子树视为独立部分并安全地跳过其渲染。
收益程度
在 web.dev 上发布的演示中,谷歌将一个结构复杂的页面拆分后,将其渲染时间从 232 毫秒缩短至 30 毫秒,提升了约七倍。需注意的是,这只是一个专门制作的演示页面,并非实际生产环境中的网站。对于上述那种真实的产品列表页面,更实际的提升幅度约为两倍,这样的改进仍能让页面在首次绘制时就呈现完整状态,而非持续加载。
极端的案例则展示了该技术的极限。在一场 Chrome Dev Summit 的演讲中,开发者将这一技术应用于一个包含超过 27 万个 DOM 节点的巨大单页 HTML 规范,其布局时间从大约 50 秒降至约 400 毫秒。虽然很少有页面会达到这种程度,但它体现了为那些无人可见的内容所付出的巨大努力。
明确当前正在发生的情况很有帮助。CSS 并不会让处理器运行得更快。你实际上是在告诉浏览器,有很多工作目前不必立即执行。当一个页面包含 600 张卡片而可视区域仅显示 8 张时,一开始就渲染所有卡片往往并非对主线程的最佳利用方式。
为何“contain-intrinsic-size”并非可选属性
如果仅使用 content-visibility: auto,在滚动时会发现滚动条会跳动,这很容易被误认为是其他无关的错误。
原因很简单:那些布局被跳过的卡片没有已知的高度,因此浏览器会将其大小视为空白状态。当有数百张卡片被跳过时,整个文档的高度就会严重失准,滚动条也会反映出这个错误的高度。随着卡片逐渐进入可视区域并显示其真实大小,文档高度随之增加,滚动条的位置也会相应改变。
contain-intrinsic-size用于指定在跳过某个元素时应假设的尺寸。示例中的340px是对尚未渲染的卡片尺寸的估算值,无需追求精确度,只要能避免滚动条出现明显跳动即可。
auto关键字的作用
auto关键字很容易被忽视,但实际上非常重要。使用auto时,一旦卡片被渲染,浏览器就会记录其实际尺寸;如果该卡片后来离开可视区域再次被跳过,浏览器会使用之前记录的尺寸而非你设定的估算值。如果没有auto,每次卡片滚出可视区域时都需重新使用固定的估算尺寸。
屏幕上的卡片始终以其真实高度显示。差异会出现在高度可变的内容中:过长的产品名称需要换行显示,或者促销标签会增加一行。如果没有使用auto属性,当卡片离开可视区域且滚动位置发生变化时,其高度会恢复到估算值。请始终使用auto属性。
浏览器支持与渐进增强
该特性已不再仅限于Chrome浏览器。Chrome从85版本(2020年)开始支持content-visibility属性,Firefox从125版本开始支持,Safari则从18版本开始支持。在撰写本文时,该属性的状态被标记为“Baseline Newly Available”,即2025年9月15日已实现全面支持,意味着所有三大主流浏览器引擎都兼容该功能;如果需要支持旧版本浏览器,请查看最新的兼容性数据。
不支持该属性的浏览器会直接忽略它,依旧以惯常方式渲染所有内容。因此这是一种渐进式增强方案,不会对旧版客户端造成任何负面影响。
其与核心网页指标及SEO的关联
其背后的动机是提升性能,而分类页面正是搜索引擎团队密切关注的URL类型。这类页面属于公开内容,针对的是有价值的搜索查询,谷歌会衡量它们对真实用户的体验表现。
布局与绘制性能会影响谷歌用作排名依据的三个核心网页指标中的两个:
- 从交互到下一次绘制的时间(INP)会直接受益。由于无需渲染屏幕外的卡片,主线程得以释放,从而让触摸和点击操作能更快得到响应。
许多非商业页面都具有这种特点,从文档和新闻动态到博客文章存档、冗长的讨论帖以及多部分组成的文章皆是如此。凡是大量类似内容垂直堆叠且大部分初始位置在屏幕外,同时该页面是公开可访问的,这种变化都会影响谷歌所衡量的各项指标。
切勿承诺能提升排名。仅通过几周时间对单个网站进行观察几乎无法证明什么,而且排名取决于的指标远不止一个。合理预期的是,一旦积累了足够的真实用户样本,在PageSpeed Insights等工具中的数据就会朝着正确的方向变化。
这一优势并不仅限于公共页面。控制面板、管理表格以及内部工具同样面临600行内容的渲染成本问题。虽然登录后没有搜索方面的优势,但得益于相同的CSS样式,用户体验依然会有提升。
跳过渲染会隐藏内容,使其不被爬虫抓取吗?
这是细心的SEO专家首先会问的问题,鉴于众多懒加载方案已导致内容对爬虫不可见,这个疑问合情合理。关键区别在于:content-visibility: auto只是渲染优化手段,而非真正改变内容的可见性。内容在页面加载的那一刻就已经存在于HTML和DOM中;只是布局和绘制操作会推迟到元素接近视口时才会执行。
Googlebot的滚动方式与人类不同。它使用极高的视口进行渲染,然后再检查生成的DOM结构。那些被跳过但实际存在的卡片也是该DOM的一部分,因此每个产品标题和价格都会像其他内容一样被索引。
这与旧式的处理方式形成对比——那种方式只有在发生滚动事件时才会插入内容。由于爬虫不会生成滚动事件,这类内容确实可能无法被索引到。content-visibility不会导致这个问题,因为它从一开始就不会插入任何内容,所有内容都早已存在。
有一个注意事项与属性本身无关。如果页面通过客户端 JavaScript 来生成内容,就会出现独立的 SEO 问题。Googlebot 虽然会执行 JavaScript,但渲染会被排队处理,速度更慢且可靠性更低,而且许多其他爬虫对 JavaScript 的处理能力也很差。对于公开内容,应在服务器端渲染 HTML,并在 CSS 中添加 content-visibility 属性。这样的组合既能保证内容可被爬取,又能提升相关指标的表现。
与无限滚动、虚拟化及自定义观察器相比
无限滚动、分页 API 以及虚拟化都是为了解决同样存在的长页面加载缓慢的问题,因此值得进行客观比较。
滚动加载与分页 API
在用户滚动时加载更多内容,是从数据层解决该问题的方法。当数据集确实非常庞大、无法一次性全部发送到浏览器时,这是最佳选择。
这样做会有成本。你需要修改 API、处理加载状态、添加滚动监听器,并追踪已获取的数据状态。用户体验也会受到影响:页面内搜索功能无法定位尚未加载的项,而滚动到列表末尾也会变得十分繁琐。在公共分类页面上,还会增加 SEO 方面的负担,因为只有在滚动后才会显示的产品对爬虫来说是不存在的;你需要使用分页的备用 URL 以及额外的标记结构,才能让这些内容被索引。对于那些已经存在于 HTML 中的 600 张卡片而言,这意味着不仅要重新编写代码,还要进行额外的 SEO 处理,以解决实际上属于渲染层面的问题。
虚拟化库
借助 react-window 或 TanStack Virtual 等库实现的虚拟化技术,会将完整的数据集保留在内存中,而对于可视窗口之外的内容则丢弃对应的 DOM 节点。这种方法确实有效,在处理极其庞大的数据量时也是更好的选择,具体原因将在下文中阐述。
其代价在于需要依赖 JavaScript、重新编写组件,以及处理高度不固定的行这一极其棘手的问题。由于被移除的元素根本不在 DOM 中,因此“页面内查找”功能无法检测到它们,屏幕阅读器也难以处理这些元素,而在公共页面上,爬虫更会忽略它们。
手动实现的 IntersectionObserver 方法
当 IntersectionObserver 检测到元素可见时再自行渲染这些元素,实际上是在用主线程 JavaScript 重复实现 content-visibility: auto 已经具备的功能,同时还可能重新引入浏览器早已原生解决的滚动锚定问题。如今几乎没有理由再使用这种方法。
如何选择
content-visibility的真正优势并非它能取代这些技术,而在于成本要低得多。它只是一个CSS属性:无需JavaScript,无需修改API,也无需重新编写代码,而且内容会保留在DOM中,便于搜索、辅助技术使用以及页面内查找。
必须明确说明其中的权衡。content-visibility能节省渲染开销,但无法减少内存占用。每个DOM节点依然存在。当卡片数量为600张时,这点影响可以忽略不计;但一旦达到5万或10万条记录,DOM的规模本身就会成为一个问题,这时就需要借助虚拟化技术来应对复杂性。在选择工具之前,先确定您面临的是渲染成本问题还是DOM规模问题。
适用场景与潜在缺陷
这不是一个可以随处使用的属性。它在相同结构重复出现较多的场景下效果最佳,例如:
- 目录或列表中的卡片
任何由多个类似区块垂直堆叠而成、且大部分在加载时位于屏幕外的内容,都是适合测试的候选对象。
测量被跳过的内容会导致错误数值
该属性会与那些需要在子树渲染完成之前获取其精确几何信息的代码产生冲突。典型的例子就是尝试读取仍位于屏幕外的行中的内部元素的高度。
const height = row
.querySelector('.details')
.getBoundingClientRect()
.height;
在元素尚未被渲染时,使用 getBoundingClientRect() 测量其内部内容会得到零值或错误的结果。该元素自身的框模型显示的是来自 contain-intrinsic-size 的占位尺寸,这一数值同样可能与实际情况不符。在真实界面中,这类代码很常见,例如在以下场景:
- 将工具提示定位在触发元素旁边
- 计算动画的起始与结束值
- 确定下拉菜单的显示位置
- 为虚拟列表设置行高
- 实现固定定位逻辑
- 让某个组件与另一个组件对齐
如果在内容可见之前就需要精确的几何尺寸,那么在添加相关属性之前务必进行充分测试。
其他不合适的选项
- 固定标题,以及那些只有在所有子元素同时拥有实际几何形状时才能正确计算的布局。
- 视口之外的内容。这类内容无论如何都必须立即渲染,因此该属性并无实际好处,反而会增加一些额外的处理工作。
- 视觉效果超出其边界范围的元素。由于该属性会限制绘图范围,那些溢出的内容,如较大的阴影或置于卡片内的弹出窗口,会被裁剪在卡片的边缘。
两个较少人知的细节
隐藏的值
content-visibility: hidden 的作用与 display: none 类似,都能跳过元素的渲染,但浏览器会保留该元素的渲染状态缓存。要再次显示该元素的成本远低于让 display: none 的元素重新渲染,因为无需从头开始执行渲染工作。正因如此,它非常适合用于标签页、屏幕外菜单以及虚拟滚动器。与 auto 不同,处于 hidden 状态下的内容在隐藏时无法通过“页面内查找”功能访问。
响应跳过状态的变化
每当使用 content-visibility: auto 的元素在跳过渲染与正常渲染状态之间切换时,浏览器会触发 contentvisibilityautostatechange 事件。通过监听该事件,你可以暂停那些对于浏览器无需渲染的内容而言较为耗资源的脚本,比如 Canvas 绘图操作。
在自己的页面上验证其效果
一个简单的实验就能让差异显而易见。创建一个包含约1,000张卡片的测试页面,并添加一个用于切换content-visibility: auto状态的开关。打开开发者工具,进入性能面板,先在关闭开关的情况下记录页面重新加载的过程,然后再在打开开关的情况下记录一次,接着比较两次记录中的紫色布局块。
随后将同样的方法应用到你最长的实际生产页面上。在页面加载时捕获性能轨迹。当布局部分占据主导地位而交互功能迟迟无法实现时,为那些重复出现的元素添加content-visibility: auto通常是最经济有效的改进方式:仅需一个属性,无需重写代码,也无需更换框架。
关键要点
- 浏览器会为整个文档处理样式和布局;
content-visibility: auto允许它们推迟处理远离视口区域的元素,而无需从 DOM 中移除任何内容。 - 必须在同一项更改中同时使用
contain-intrinsic-size: auto <estimate>,否则滚动条位置会跳动。 - 内容依然可索引、可搜索且易于访问,这使其有别于滚动加载和虚拟化技术。
- 它节省的是渲染时间而非内存;一旦 DOM 本身变得过大,虚拟化才是正确的解决方案。
- 避免在可视区域上方、在显示前就需要测量几何尺寸的元素上,以及在使用超出自身边界绘图的组件上使用该属性。