← 文章 / 编程开发
freeCodeCamp 1小时前 · 2026-10-02 00:28:13 · 0 阅读

揭秘 CSS content-visibility 机制:如何提升页面渲染性能

如果一个页面里有 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 包含性(CSS containment)。正如 web.dev 指南 所述,content-visibility: auto 会应用布局、样式和绘制包含。当内容对用户不重要时,浏览器可以跳过该子树中更多的处理工作。

因此,在没有 content-visibility 的情况下,浏览器仍可能对视口下方很远处的内容执行渲染工作。而有了它,其中部分工作可以在内容变得重要之前被跳过。

注意这里的关键词是可以(can)。

content-visibility: auto 并不保证视口外的每个元素其渲染工作都会被跳过。由浏览器来判断内容是否对用户相关,以及是否可以跳过其渲染。

听起来很有用。但到底有用多少?

是时候去测量了。

我搭建了一个包含 150 张卡片的页面

我不想在一个小的演示中测试,那样差异可能会淹没在测量噪声里。

所以我故意把页面做得有点“离谱”。它包含了150 个内容繁重的卡片。

每个卡片包含:

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

  • 一个标题和标签

  • 八个段落

  • 十个相关条目

数字 150 本身并没有什么特殊含义。

一个页面不会因为跨越了某个元素数量阈值,就突然变成 content-visibility 的理想候选对象。

例如,150 个简单的 <div> 元素可能让浏览器几乎没有昂贵的工作可以跳过。

但这 150 个包含嵌套布局、文本、列表、图片等大量渲染工作的区块,正好能制造出明显的测试效果。

对于这个实验来说,这正是我想要的。

我也没有用 React 或其他框架,而是直接用原生 HTML、CSS 和 JavaScript。

这是有意为之的。

我们要测试的是 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(`
    <article class="card">
      <div class="card-image-placeholder"></div>

      <h2>Product ${i}</h2>

      ${Array.from(
        { length: PARAGRAPHS_PER_CARD },
        (_, index) => `
          <p>
            Product ${i}, paragraph ${index + 1}.
            This is sample content used to make
            each card more expensive to render.
          </p>
        `
      ).join("")}

      <ul>
        ${Array.from(
          { length: RELATED_ITEMS_PER_CARD },
          (_, index) => `
            <li>Related item ${index + 1}</li>
          `
        ).join("")}
      </ul>
    </article>
  `);
}

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

我考虑过使用真实图片,但那会让实验变复杂。

网络延迟、缓存和图片解码都可能影响测试结果。

所以每张卡片改用 CSS 占位块:

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

每组测试我都保持了相同的浏览器环境和视口尺寸,并且在录制首屏加载时不滚动页面。

我还多次重复测试,而不是挑最好看的一次结果。

如果你想复现这个实验,完整示例已发布在 GitHub 上。

其中包含与下文测量数据相同的测试页面和配置,你可以在自己的浏览器和设备上运行并对比结果。

现在我们有东西可以测量了。

先看基准测试

在添加 content-visibility 之前,我使用 Chrome DevTools 的 Performance 面板对页面进行了三次录制。

Performance 录制的 Rendering 活动数据如下:

运行次数 渲染耗时
1 39 ms
2 44 ms
3 42 ms
中位数 42 ms

我选择的是中位数,而非最快的那次运行。

这里有个重要的澄清:这些数字代表的是 DevTools Performance 录制中报告的 Rendering 活动。

它们并非页面总加载时间、Core Web Vitals 指标,也非用户感知性能的直接度量。

Chrome DevTools 将 Rendering 作为 Performance 录制中活动拆解的一个类别来报告,因此我将始终特指 Rendering 活动,而不是笼统地说页面"在 42 ms 内完成渲染"。

所以我们的基准线是:

Rendering 活动中位数:42 ms

Chrome DevTools 的 Performance 录制截图,展示了未添加 content-visibility 的基准测试中的 Rendering 活动。

基准 Performance 录制截图。上述的 42 ms 中位数是通过三次独立运行计算得出的。

接着,我只改变了一个变量:

.card {
  content-visibility: auto;
}

然后再次进行实验。

运行次数 渲染耗时
1 20 ms
2 21 ms
3 20 ms
中位数 20 ms

好的,效果很明显。

我们从:

42 ms → 20 ms

即:

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

在这个实验中,添加 content-visibility: auto 与 Chrome DevTools 报告的 Rendering 活动减少约 52% 相关。

Chrome DevTools Performance recording after applying content-visibility: auto to the cards.

但这个数字需要谨慎看待。

这并不意味着 content-visibility 能让网站快 52%,甚至也不代表整页加载快了 52%。

我们只是在 Chrome DevTools 中,针对一个测试环境下特定页面的某类活动做了测量。

实际效果会受以下因素影响:

  • 视口外的内容量

  • 这些内容的渲染成本

  • 浏览器类型

  • 设备性能

  • 视口大小

  • 页面结构

我们的实验基本是让 content-visibility 有大量工作可以跳过。

因此,有用的结论不是:

“content-visibility 让网站快 52%。”

而是:

对于包含大量屏外内容的页面,让浏览器跳过不必要的渲染工作,可以带来可测量的性能提升。

Diagram showing content-visibility rendering visible cards inside the viewport while allowing rendering work for off-screen cards to be skipped.

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 搭配使用也非常有价值。

既然我们要跳过内容渲染,就得保证页面在内容渲染前仍有合理的几何尺寸。

回退值应该设多大?

我的第一个疑问是:回退值设大一点还是小一点,会不会明显影响首次 Rendering 指标?

于是我试了三个值:

回退值 Rendering
100px 12 ms
900px 10 ms
2000px 11 ms

结果非常接近。

在这种量级下,差异小到不足以证明某个回退值比其他的更快。

我们不能据此得出结论:

"固有尺寸越小越快。"

同样也不能得出:

“与真实尺寸估算越接近,渲染性能通常越好。”

但这并不是 fallback 的职责所在。

更有意义的问题是:布局会发生什么变化?

如果你的 fallback 设为 100px,而实际卡片高度远超此值,当内容渲染完成时,页面可能需要调整几何结构。

反过来,如果 fallback 远大于实际内容,同样会出现问题。

因此,你需要的是一个合理的近似值,而不是某个神奇的性能数值。

不过在测试过程中,我发现了一个意料之外的现象。

仅使用

.card {
  content-visibility: auto;
}

之后我进行了三次测量:

20 ms
21 ms
20 ms

Median: 20 ms

接着添加了:

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

结果如下:

10 ms
9 ms
12 ms

Median: 10 ms

没错,在这个特定实验中,添加显式 fallback 确实带来了 Chrome DevTools 中 Rendering 活动的进一步下降。

容易得出的结论是:

contain-intrinsic-size = 2× faster

不对。我们的测量结果只说明了这次实验中发生了什么。

它并未确立 contain-intrinsic-size 的通用性能特征。

它的职责是在尺寸包含生效时提供有用的固有几何信息,包括在无法获取正常渲染尺寸时提供 fallback。

对这些数字还需谨慎对待。最初的基线对比用了三次运行,而这些后续探索性测量也用了三次运行。

对于实验中的观察而言,这没问题。但这并非用于宣称某种配置普遍快于另一种受控方法论。

因此,10 ms 的结果保持原样:它是这次实验中一个有趣的观察结果,而非浏览器承诺。

我们是否只是把工作转移到了滚动阶段?

初始加载基准测试无法回答另一个问题。

如果我们跳过了屏幕外卡片的工作,那么当用户滚动时,其中一些卡片最终会变得相关。

工作并没有神奇消失。部分工作被推迟到浏览器判定内容相关时再执行。

我们究竟改善了整体体验,还是只是把部分工作转嫁到了别处?

这次实验没有基准数据能直接回答这个问题。

我没有记录受控的滚动测试,所以不会把手动滚动时的直观感受转化为新的性能论断。

要回答这个问题,需要一项独立的实验,重点关注滚动过程、卡片进入相关区域、布局偏移,以及页面滚动时的帧行为。

目前,我们的测量结果只能说明一个更窄范围的事实:

在此特定测试中,content-visibility: auto 减少了初始 Rendering 阶段的活动量。

这并不意味着浏览体验的每个环节都加速了 52%。这是解读当前数据时一个重要的界限。

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

到这里,content-visibility 听起来确实和懒加载很像。

两者的目的都是避免无谓的开销,但它们针对的通常是不同的工作。

懒加载主要问的是:

我现在需要加载这个资源吗?

而 content-visibility 问的是另一回事:

我现在需要渲染这些内容吗?

以图片为例:

<img
  src="/product.jpg"
  loading="lazy"
  alt="Black running shoes"
>

原生的图片懒加载可以推迟加载资源,直到它接近被需要的时候。

但使用:

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

元素可能已经存在于 DOM 中,其资源也早已加载完毕。

此时我们在询问浏览器,是否需要对这部分内容执行渲染工作。

因此,这两种优化未必是互斥的。

你可以在同一个页面上同时使用它们。

  • 一种机制有助于避免过早加载资源。

  • 另一种机制有助于避免当前不必要的渲染开销。

Diagram showing two page performance strategies: lazy loading delays loading resources such as images and JavaScript, while content-visibility can defer layout and painting for off-screen content.

但可访问性怎么办?

content-visibility: auto 有个容易被忽略的细节。

被跳过渲染的屏幕外内容依然留在 DOM 中,并且仍可能出现在无障碍树(accessibility tree)里。

这对性能是好事,但也划清了一条重要界限: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。但当页面包含大量昂贵的屏幕外内容时,它能为浏览器提供一项宝贵选项:干脆不渲染用户还看不到的部分。

参考资料

原始来源: freeCodeCamp

评论 (0)