← 返回蜂巢洞察

CSS中的“内容可见性”机制是如何工作的,以及它如何能够提升渲染性能

当一个页面包含150张内容量很大的卡片,但用户只能看到前几张时,会发生什么呢? 你可能会认为浏览器只会处理当前可见的内容。但实际上并非如此。 即使某些内容位于视口之外数千像素的位置,浏览器仍然可能需要对其进行渲染处理。 因此我想尝试一种方法:如果我们能够告诉浏览器:“目前不需要渲染这些内容,用户还看不到的部分可以跳过不渲染”,会怎么样呢? 浏览器无需立即渲染所有这些内容,对于用户暂时看不到的部分,可以直接跳过渲染步骤。 CSS中有一个属性可以帮助我们实现这一目标,而且只需用一行代码即可完成: .card { content-visibility: auto; } 这自然让我很好奇:这种设置究竟

当一个页面包含150张内容量很大的卡片,但用户只能看到前几张时,会发生什么呢?

你可能会认为浏览器只会处理当前可见的内容。但实际上并非如此。

即使某些内容位于视口之外数千像素的位置,浏览器仍然可能需要对其进行渲染处理。

因此我想尝试一种方法:如果我们能够告诉浏览器:“目前不需要渲染这些内容,用户还看不到的部分可以跳过不渲染”,会怎么样呢?

浏览器无需立即渲染所有这些内容,对于用户暂时看不到的部分,可以直接跳过渲染步骤。

CSS中有一个属性可以帮助我们实现这一目标,而且只需用一行代码即可完成:

.card {
  content-visibility: auto;
}

这自然让我很好奇:这种设置究竟能带来多大的效果呢?

于是我没有仅仅停留在阅读文档上,而是实际创建了一个包含150张内容量很大的卡片的页面,然后使用Chrome DevTools进行了测试。

结果超出了我的预期……但同时也引发了一个新的问题。

首先,让我们来了解一下content-visibility这个属性实际上是在要求浏览器做什么。

我们将讨论的内容:

先决条件

要跟随本文进行实验,你需要具备以下条件:

  • 对HTML和CSS有基本的了解

  • 拥有一款现代浏览器,比如Chrome

  • 对Chrome DevTools有一定的熟悉度

你不需要掌握任何框架知识。这个实验使用的都是纯HTML、CSS和JavaScript,因此我们可以专注于浏览器的渲染行为本身。

content-visibility: auto到底有什么作用?

content-visibility这个属性用于控制元素是否需要显示其内容。

在这个实验中,我们关注的是其中一个具体的值:

.card {
  content-visibility: auto;
}

当设置为auto时,如果某个元素当前对用户来说并不重要(比如它位于视口之外),浏览器就可以跳过对该元素的渲染处理。

需要注意的是,这里讨论的是渲染过程。

该元素并未从DOM中被移除。而且,content-visibility这一属性并不会直接告诉浏览器不要下载与该元素相关的资源。

我们实际上是给了浏览器一个机会,让它能够跳过那些目前对用户来说并无用的渲染工作。

这一机制是通过CSS的包含规则来实现的。正如web.dev的指南中所解释的那样,当设置content-visibility: auto时,浏览器会继续对相关内容进行布局、样式处理和渲染工作;但当这些内容对用户来说并不重要时,浏览器就可以跳过对这些内容的渲染。

因此,如果没有content-visibility这一属性,浏览器仍然可能会对那些位于视口之外的内容进行渲染;而有了这个属性,某些渲染工作就可以被跳过,直到相关内容真正变得对用户有用为止。

注意这里用了“可以”这个词。

content-visibility: auto并不能保证所有位于视口之外的元素都会被浏览器忽略而不进行渲染。浏览器会自行判断这些内容是否对用户有用,以及是否可以跳过它们的渲染过程。

这听起来确实很有用,但实际效果究竟有多明显呢?

是时候通过实验来验证一下了。

我制作了一个包含150张卡片的页面

我不想在一个规模很小的演示环境中进行测试,因为在这种环境下,测量结果可能会受到各种因素的干扰而无法准确反映实际情况。

因此,我故意设计了一个内容量非常大的页面——这个页面包含了150张信息量很大的卡片。

每张卡片包含以下内容:

  • 一个固定大小的图片占位符

  • 一个标题以及一些标签

  • 八段文字

  • 十项相关内容

数字“150”本身并没有什么特别的意义。

仅仅因为一个页面包含的元素数量超过了某个阈值,并不意味着它就适合使用content-visibility这一属性。

例如,如果一个页面只包含了150个简单的

元素,那么浏览器可能几乎没有需要跳过的渲染工作。

但如果你有一个包含嵌套布局、文本、列表、图片等多种元素的页面,那么使用content-visibility就会带来更大的效果。

在我的这个实验中,正是想要验证这种效果。

此外,我在编写代码时也使用了纯HTML、CSS和JavaScript,而没有使用React或其他框架。

这是我有意为之的。

如果我们要测试CSS渲染优化效果,那么添加框架相关的代码只会给我们带来额外的干扰因素,而这些因素其实并不是我们真正需要的。

以下是用于生成这些卡片的JavaScript代码:
const CARD_COUNT = 150;
const PARAGRAPHS_PER_CARD = 8;
const RELATED_ITEMS_PER_card = 10;

const cards = [];

for (let i = 1; i <= CARD_COUNT; i++) {
  cards.push(`
    

产品${i}</h2>> ${Array.from( { length: PARAGRAPHS_PER_card }, (_, index) => `

产品${i},第${index + 1}段内容。 这些内容只是为了增加渲染的复杂性而已。

</p> ` ).join("")}

    ${Array.from( { length: RELATED_ITEMS_PER_CARD }, (_, index) => `
  • 相关项目${index + 1}</li> ` ).join("")} <\/ul>

> `); } document.querySelector("#feed").innerHTML = cards.join("");

我考虑过使用真实的图片,但那样会让实验结果变得更加复杂。

网络延迟、缓存以及图像解码等因素都可能影响我们最终看到的效果。

因此,每张卡片实际上都使用了CSS占位符来替代真实图片:

.card-image-placeholder {
  height: 320px;
  background: linear-gradient(
    135deg,
    #e5e7eb,
    #f3f4f6
  );
}

在每次测试中,我都会保持浏览器环境及视口尺寸不变,并且在初始加载阶段不会进行滚动操作。

我还会重复多次实验,而不是只选择一次显示效果最好的结果来进行分析。

如果你想重现这个实验,我已经在GitHub上发布了完整的示例代码。

其中包含了用于测试的页面和配置信息,你可以自己运行实验,并在你的浏览器和设备上对比实验结果。

现在我们已经有可以用来衡量实验效果的数据了。

首先,基线数据

在添加content-visibility这个属性之前,我使用Chrome DevTools的性能分析工具记录了三次页面加载情况。

性能分析记录中显示的渲染时间如下:

测试次数 渲染时间
1 39毫秒
2 44毫秒
3 42毫秒
中位数 42毫秒

我选择的是中位数,而不是最快的一次测试结果。

需要特别说明的一点是:这些数字代表的是DevTools性能分析工具记录中的“渲染时间”,

它们并不等于页面的总加载时间,也不属于Core Web Vital指标,更不能直接反映用户实际感受到的性能表现。

Chrome DevTools将“渲染时间”作为性能分析数据中的一个分类项来显示,因此我在描述实验结果时会明确指出是“渲染时间”,而不会说“页面在42毫秒内完成渲染”。

所以我们的基线数据就是:

中位数渲染时间:42毫秒

Chrome DevTools性能分析记录,显示了未使用content-visibility属性时的页面渲染时间。

这是一份基线性能分析记录,上面显示的42毫秒中位数是通过对三次独立测试结果计算得出的。

接下来,我只改变了一个设置:

.card {
  content-visibility: auto;
}

然后再次进行了实验。

测试次数 渲染时间
1 20毫秒
2 21毫秒
3 20毫秒
中位数 20毫秒

好吧,这个效果其实相当明显,并不隐蔽。

我们之前的测试结果是:

42毫秒 → 20毫秒

或者:

(42 - 20) / 42 × 100 ≈ 52%

在这个实验中,添加 `content-visibility: auto` 后,Chrome DevTools显示的渲染时间确实缩短了大约 52%。

在卡片上应用 content-visibility: auto 后,Chrome DevTools记录的性能数据。

但是我们对这个数据需要谨慎对待。

这并不意味着 `content-visibility` 能让网站的速度提高52%,甚至也不意味着整个页面的加载速度会提升52%。

我们是在一个特定的测试环境中,使用一个内容量较大的页面,通过Chrome DevTools来测量某一类渲染活动。

最终结果会受到很多因素的影响,比如:

  • 页面中位于可视区域之外的内容有多少

  • 渲染这些内容需要消耗多少计算资源

  • 所使用的浏览器

  • 设备型号

  • 视口设置

  • 页面的结构设计

我们的实验实际上是为了让 `content-visibility` 有更多的内容可以跳过而不进行渲染。

所以,正确的结论并不是:

"content-visibility能让网站的速度提高52%。"

而应该是:

对于那些包含大量屏幕外内容的页面来说,让浏览器能够跳过不必要的渲染工作,确实可以带来明显的性能提升。

示意图显示:在视口范围内显示可见的内容,同时允许浏览器跳过屏幕外内容的渲染。

web.dev也用他们自己的示例验证了这一观点,并且同样观察到了显著的性能提升。

但是他们的数据只是他们自己测试的结果,而我们的数据则是我们自己实验得出的结论。

这两个数字都不适合直接被你应用到自己的项目中,因为你需要先进行自己的测试才能得出准确的结果。

不过我们的实验还没有结束。因为在开始滚动页面后,又出现了一个问题。

我们节省了渲染时间,但现在布局出现了问题。

想想我们刚刚告诉浏览器什么:某个卡片位于视口之外,因此可以跳过它的渲染。

这个道理没错,但页面仍然需要一个合理的布局结构。

那么这里就有一个棘手的问题:一个屏幕外的卡片在什么时候才能被浏览器识别为需要正常显示的元素呢?

如果浏览器最初估计的该卡片的尺寸与实际尺寸不符,那么当这个卡片变得可见并被渲染时,布局系统才会进行调整。

contain-intrinsic-size正是为了解决这个问题而存在的。

<我们可以为浏览器提供一个备用的“内在尺寸”:

.card {
  content-visibility: auto;
  contain-intrinsic-size: auto 900px;
}

这里的`900px`这个数值为浏览器提供了一个备用值,当页面内容被跳过且没有可用的渲染尺寸信息时,浏览器就可以使用这个数值来进行渲染。

<但`auto`这个设置使得这一机制变得更加有趣。

<想象一下,如果这张卡片还没有被正常渲染出来,浏览器就没有之前可以参考的尺寸信息,因此`900px`这个值就可以作为备用方案来使用。

<后来,当这张卡片靠近视口范围并被正常渲染时,浏览器就能得到它的实际渲染尺寸了。

<如果这张卡片再次被跳过,浏览器就可以直接使用之前记录下的尺寸信息进行渲染,而不再依赖`900px`这个备选值。

<所以:

contain-intrinsic-size: auto 900px;

这并不意味着浏览器真的知道这张卡片的实际高度是`900px`。

<它的真正含义是:

流程图说明:当有可用的实际渲染尺寸时,`contain-intrinsic-size`会使用该尺寸;如果没有记录到实际尺寸,则会使用`900px`作为备选值。

这个属性与`content-visibility`结合使用也会非常有用。

<即使我们决定跳过某些内容,在这些内容被渲染之前,页面仍然需要保持合理的布局结构。

那么,备选值应该设定为多少呢?

我的第一个问题是:选择较小的或较大的备选值,是否会对初始的渲染性能产生明显影响。

<为此,我尝试了三个不同的数值:

备选值 渲染时间
100px 12毫秒
900px 10毫秒
2000px 11毫秒

这些测试结果相差非常小。

<在这个范围内,这些差异并不足以证明某个备选值比另一个更高效。

<我们因此不能得出这样的结论:

"较小的内在尺寸会带来更快的渲染速度。"

<同样,我们也不能得出这样的结论:

"与实际尺寸最接近的估算值总是能获得最好的渲染效果。"

<因为备选值的真正作用并不是为了确保最快的渲染速度,也不是为了让估算值与实际尺寸完全吻合。

><更值得关注的是:这种设置会对页面布局产生什么样的影响。

<如果你的备选值是`100px`,但实际卡片的高度远高于这个数值,那么在内容被渲染时,页面的布局可能需要进行调整。

<相反,如果备选值远大于实际内容的高度,也会导致类似的问题。

<因此,我们需要的只是一个合理的近似值,而不是一个能够保证绝对高效的性能数字。

<但在测试这个功能的过程中,我注意到了一些意料之外的事情。

仅通过添加以下代码:

.card {
  content-visibility: auto;
}

之后我又进行了三次测量:

20 毫秒
21 毫秒
20 毫秒

中位数:20 毫秒

然后我又添加了这段代码:

.card {
  content-visibility: auto;
  contain-intrinsic-size: auto 900px;
}

测量结果如下:

10 毫秒
9 毫秒
12 毫秒

中位数:10 毫秒

因此,在这个特定的实验中,添加这段代码确实使得 Chrome DevTools 的渲染耗时进一步减少了。

一个看似合理的结论是:

使用 contain-intrinsic-size 可以使性能提升 2 倍

但实际上,并非如此。我们的测量结果只说明了在这个实验中发生了什么,并不能证明 contain-intrinsic-size 总能带来性能提升。

contain-intrinsic-size 的作用是在需要控制元素大小时提供有用的几何信息;当无法获取元素的正常显示尺寸时,它也会提供一个备用方案。

另外,还需要注意这些测量数据的可靠性。最初的基准测试进行了三次测量,而后续的探索性测试也同样进行了三次测量。对于在实验中观察到的结果来说,这样的重复测量是可行的;但若要用这些数据来断言某种配置方式普遍比另一种更高效,那么这种做法就不合适了。

10 毫秒 这个结果仅仅说明:这是个有趣的观察结果,并不能作为浏览器性能的可靠指标。

我们是不是只是把部分处理任务转移到了滚动操作中去了?

初始加载基准测试并没有回答另一个问题:如果我们暂时忽略了那些不在屏幕显示范围内的卡片,那么当用户开始滚动页面时,其中一些卡片最终还是会变得需要被显示出来的。

这些需要处理的任务并没有凭空消失,只是被推迟到了用户认为这些内容需要被显示的时候才进行处理而已。

那么,我们真的是改善了整体的使用体验吗?还是仅仅把部分处理任务转移到了其他地方而已呢?

目前我还没有针对这个问题的测量数据。由于我没有进行过控制严格的滚动测试,因此也不能根据手动滚动的观察结果来断言某种配置方式更高效。

要真正了解这个问题,就需要进行专门的实验,来研究滚动操作、卡片内容的显示时机、布局的变化以及页面滚动时的各种表现。

目前来说,我们的测量结果只能说明:content-visibility: auto 这个属性确实减少了这次测试中的渲染耗时。

但这并不意味着整个浏览体验的性能都提升了 52%——这个数字其实蕴含着重要的限制条件。

等等,这不就是懒加载吗?

在这个背景下,content-visibility 这个属性听起来确实与懒加载有些相似之处。

两者都在试图避免不必要的工作,但它们所避免的工作类型通常是不同的。

“延迟加载”主要是在询问:“我现在还需要加载这个资源吗?”

我目前真的需要渲染这些内容吗?

而content-visibility则在问另一个问题:“我现在就需要显示这些内容吗?”

我现在真的需要呈现这些内容吗?

<img
  src="/product.jpg"
  loading="lazy"
  alt="黑色跑鞋"
>

对于原生图像,延迟加载可以让系统等到真正需要这些资源时才进行加载。

但是,如果使用以下CSS代码:

.product-card {
  content-visibility: auto;
}

那么相关元素可能会早就存在于DOM中,其资源也可能已经被加载完成了。

这时我们实际上是在询问浏览器:是否现在就需要对这些内容进行渲染处理。

因此,这些优化措施并不一定是互斥的——你完全可以在同一个页面上同时使用它们。

  • 一种优化方式可以帮助避免过早加载资源。

  • 另一种优化方式则可以避免执行当前并不必要的渲染操作。

图表展示了两种页面性能优化策略:延迟加载会推迟图像、JavaScript等资源的加载,而content-visibility则可以延缓对屏幕外内容的布局和渲染。

但是,对于无障碍访问功能来说呢?

关于content-visibility: auto,有一个很容易被忽略的细节。

那些被跳过渲染的屏幕外内容仍然会存在于DOM中,也会出现在无障碍访问树中。

这一特性确实有助于提升性能,但它也为我们指明了一个重要的界限:content-visibility: auto只是一种渲染优化机制,并非一种用于隐藏内容的手段。

如果你的目的是让辅助技术无法看到某些内容,请不要使用content-visibility来实现这个目的。应该使用适当的HTML、CSS以及无障碍访问相关的技术来达到这个效果。

还有一点需要注意:web.dev指出,即使某些内容通常会被display: none或visibility: hidden这样的样式隐藏,但它们仍然有可能出现在无障碍访问树中。

因此,如果你在复杂的交互式页面上使用了content-visibility,请务必实际测试一下用户使用键盘或辅助技术时的体验,而不要想当然地认为这种优化不会影响无障碍访问功能。

那么,究竟在什么情况下使用content-visibility才是有意义的呢?

经过种种测试后,我们得出的结论其实很简单:添加这个CSS属性确实可以让你的网站运行得更快。

这很可能是一个好迹象。

当你的页面中包含大量位于视口之外的、需要耗费较多渲染资源的元素时,content-visibility: auto这个属性就显得非常有用了。

你可以考虑以下这些情况:

  • 长篇文章或社交媒体的动态列表

  • 大型产品详情页面

  • 包含多个章节的文档页面

  • 长度较长的控制面板

  • 结构复杂的页面内容

如果你的页面规模较小,几乎所有内容都能立即被用户看到,那么也就没有什么需要优先处理的渲染工作了。

但在将这个属性应用到实际生产环境中之前,还有一个实际问题需要考虑:浏览器是否能够支持这个属性?

对于现代浏览器来说,对content-visibility的支持程度相当高。由于这个属性属于Baseline 2024规范的一部分,因此除非你的项目需要兼容较旧的浏览器版本,否则兼容性已经不再是一个值得担忧的问题了。

所以,并不是每个页面都需要使用content-visibility这个属性。但当一个页面中包含大量需要耗费较多渲染资源的元素时,这个属性就能为浏览器提供很大的帮助:让它可以选择不渲染那些用户目前还看不到的内容。

参考资料

相关文章

技术实践

如何在JavaScript中使用全屏API(并通过“唤醒锁API”使屏幕保持处于唤醒状态)

迟早,大多数前端开发人员都会遇到这样一个问题: "这个内容能覆盖整个屏幕吗?" 无论是幻灯片、视频播放器、信息亭的控制面板、游戏界面、绘图工具,还是教室投影仪上的计时器,当浏览器标签页和地址栏还显示在屏幕边缘时,这些界面看起来总是不完整。 好消息是,浏览器本身提供了解决方案: 全屏API 。不过坏消息是,这个API存在一些问题,在实际使用中很可能会遇到麻烦——比如用户的手势操作、Safari浏览器特有的前缀、某些iPhone设备的不兼容性,以及屏幕在进入全屏状态两分钟后就会自动变暗的问题。 在本教程中,你将学习如何: 将任何元素(或整个页面)设置为全屏显示,然后再恢复正常视图 当用户按下 Es

阅读全文
技术实践

在人工智能时代如何提升自己的能力:从技术文档编写者成长为开发人员培训师

直到最近,构建一份技术写作作品集其实很简单:只需创建一个网站,添加一些文章列表,描述自己的写作经验,并链接到自己的社交媒体账号即可。 但如今,这样的做法已经不够了。 开发人员现在可以让人工智能助手在几秒钟内生成API的使用说明、总结相关文档、编写教程大纲,甚至完成一篇编程文章的初稿。 这种情况改变了技术写作工作者应该具备的能力和技能。 如今,最重要的能力不再是仅仅写出技术上正确的句子,而是要深入了解相关技术,知道该写什么内容,验证这些说明是否有效,找出开发人员可能会遇到的问题,并将这些知识转化为可供他人实际使用的文档。 这就是我创建 这份作品集网站 的初衷。 我构建这个网站的目的并不仅仅是为了

阅读全文
技术实践

游戏手柄API在欺骗你:一篇关于如何使用JavaScript读取控制器输入信息的实用指南

游戏手柄API是您使用过的最小的浏览器API之一。它仅有四个属性、一个函数,而且不需要任何权限请求。只需大约十五行代码,就能在屏幕上显示出一个游戏控制器的外观。 不过,这十五行代码也会“悄悄地”忽略那些出现故障的游戏控制器。 我是通过自己动手开发了一个基于浏览器的控制器测试工具才了解到这一点的。有位用户发来邮件说,尽管他在所有游戏中都发现游戏手柄的摇杆在不断漂移,但该网站却显示他的手柄是正常的。事实证明他是对的——浏览器实际上返回的是错误的数据。 这篇文章介绍了GamepadAPI中那些未在官方规范文档中提及的部分,以及这些部分是如何让我花费了大量时间进行调试的:为什么必须定期向硬件发送请求以

阅读全文
技术实践

Nuxt 4.5:实验性的SSR流式渲染功能、Vite 8以及由Rsbuild提供的Rspack构建工具

Nuxt发布了4.5版本,其中包含了诸多更新:比如改用了Vite 8作为构建工具,采用了新的Rspack 2构建系统,同时还实现了实验性的SSR流式渲染功能。这种流式渲染技术能够通过立即生成HTML页面内容来缩短用户从请求开始到看到首字节内容所需的时间。此次版本还改进了错误代码处理机制,提供了新的组件模块,并为那些从早期版本升级过来的开发者提供了详细的升级指南。 作者:Daniel Curtis

阅读全文