深入了解 CSS Contain 属性

Avatar of Travis Almand
Travis Almand 发布

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

与过去相比,现代浏览器在渲染典型网页提供的 HTML、CSS 和 JavaScript 代码的复杂网络方面变得非常高效。 它只需花费几毫秒的时间,就能将我们提供的代码渲染成人们可以使用的东西。

作为前端开发人员,我们能做些什么来帮助浏览器更快地渲染?有一些常见的最佳实践,在使用现代工具时很容易忘记——尤其是在我们对生成的代码没有太多控制权的情况下。例如,我们可以控制 CSS,使用更少、更简单的选择器。我们可以控制 HTML;使树结构更扁平,减少节点数量,尤其是子节点数量。我们可以控制 JavaScript;在操作 HTML 和 CSS 时要小心谨慎。

实际上,像 Vue 和 React 这样的现代框架在最后一部分提供了很大的帮助。

我想探索一个 CSS 属性,我们可以用它来帮助浏览器确定哪些计算可以降低优先级,甚至可以完全跳过。

这个属性叫做 contain。以下是 MDN 对此属性的定义

contain CSS 属性允许作者指示一个元素及其内容尽可能独立于文档树的其余部分。这允许浏览器重新计算布局、样式、绘制、大小或它们的任意组合,仅针对 DOM 的有限区域,而不是整个页面,从而带来明显的性能优势。

简单来说,此属性提供了一种方法,我们可以向浏览器提供有关页面上各个元素之间关系的提示。不一定是指较小的元素,例如段落或链接,而是指较大的组,例如部分或文章。本质上,我们说的是包含内容的容器元素——即使内容本质上是动态的。想想一个典型的 SPA,其中动态内容在整个页面中插入和删除,通常与页面上的其他内容无关。

浏览器无法预测由于 JavaScript 在页面上插入和删除内容而可能发生的网页布局更改的未来。即使是像向元素插入类名、动画化 DOM 元素或获取元素尺寸这样简单的事情,也可能导致页面的重排和重绘。这些操作可能代价高昂,应避免或尽可能减少。

开发人员可以对未来进行某种程度的预测,因为他们会根据页面设计的 UX 了解可能的未来更改,例如当用户点击按钮时,它会调用数据以插入到当前视图中某个位置的 div 中。我们知道这是可能的,但浏览器不知道。我们还知道,在该 div 中插入数据很有可能不会从视觉上或其他方面更改页面上的其他元素。

浏览器开发人员花费了大量时间优化浏览器以处理此类情况。有各种方法可以帮助浏览器在这种情况下更高效,但更直接的提示会很有帮助。contain 属性为我们提供了一种提供这些提示的方法。

包含的各种方法

contain 属性有三个值,可以单独使用或组合使用:sizelayoutpaint。它还有两个用于常见组合的简写值:strictcontent。让我们介绍一下每个值的基本知识。

请记住,每个值都有一些规则和边缘情况,这些规则和边缘情况在规范中有所描述。我想在大多数情况下,这些情况不会引起太多关注。但是,如果您得到意外的结果,那么快速查看规范可能会有帮助。

规范中还有一个 style 包含类型,本文不会介绍。原因是 style 包含类型目前被认为价值不大,并且目前有被从规范中移除的风险。

尺寸包含

size 包含实际上很容易解释。当包含此包含的容器参与布局计算时,浏览器可以跳过相当一部分内容,因为它会忽略该容器的子元素。预期容器将具有设定的高度和宽度;否则,它会折叠,并且这是页面布局中唯一考虑的因素。它被视为根本没有任何内容。

请考虑,子元素可能会影响其容器的大小,具体取决于容器的样式。在计算布局时必须考虑这一点;使用 size 包含,很可能不会考虑这一点。一旦容器的大小相对于页面已确定,则将计算其子元素的布局。

size 包含在优化方面并没有提供太多帮助。它通常与其他值之一结合使用。

但是,它可以提供的一个好处是帮助 JavaScript 根据容器的大小更改容器的子元素,例如容器查询类型的情况。在某些情况下,根据容器的大小更改子元素会导致容器在对子元素进行更改后更改大小。由于容器大小的更改可能会触发对子元素的另一次更改,因此您最终可能会得到一个更改循环。size 包含可以帮助防止此循环。

这是一个完全人为构建的此大小调整循环概念的示例

在此示例中,点击“开始”按钮将导致红色框开始增大,基于紫色父框的大小,加上 5 个像素。当紫色框调整大小后,大小观察器会指示红色正方形再次根据父框的大小调整大小。这会导致父框再次调整大小,依此类推。代码一旦父框高度超过 300 像素就会停止此过程,以防止无限循环。

当然,“重置”按钮会将所有内容恢复到原位。

点击“设置尺寸包含”复选框将设置不同的尺寸和紫色框上的 size 包含。现在,当您点击“开始”按钮时,红色框将根据紫色框的宽度调整自身大小。没错,它超出了父框,但重点是它只调整大小一次并停止;不再有循环了。

如果点击“调整容器大小”按钮,紫色框将变宽。延迟后,红色框将相应地调整自身大小。再次点击该按钮会将紫色框恢复到其原始大小,然后红色框将再次调整大小。

虽然可以在不使用包含的情况下实现此行为,但您将错过其好处。如果这种情况在页面中经常发生,则包含有助于页面布局计算。当子元素在包含内部发生更改时,页面其余部分的行为就像从未发生过更改一样。

布局包含

layout 包含告诉浏览器外部元素既不影响容器元素的内部布局,也不受容器元素的内部布局影响。因此,当浏览器进行布局计算时,它可以假设具有布局包含的各种元素不会影响其他元素。这可以减少需要执行的计算量。

另一个好处是,如果容器在屏幕外或被遮挡,则相关的计算可能会被延迟或降低优先级。规范提供了一个示例

例如,如果包含框位于块容器的末尾附近,而您正在查看块容器的开头

具有 layout 包含的容器成为 absolutefixed 位置子元素的包含框。这与向容器应用 relative 位置相同。因此,请记住在应用此类型的包含时子元素可能会受到怎样的影响。

同样,容器获得一个新的堆叠上下文,因此 z-index 可以像应用 relativeabsolutefixed 位置一样使用。但是,设置 toprightbottomleft 属性对容器没有影响。

这是一个简单的示例

点击该框将切换 layout 包含。当应用 layout 包含时,两个紫色线条(绝对定位)将移到紫色框内。这是因为紫色框成为具有包含的包含块。需要注意的另一点是,容器现在堆叠在绿色线条之上。这是因为容器现在具有新的堆叠上下文,并相应地遵循这些规则。

绘制包含

paint 包含告诉浏览器容器的任何子元素都不会绘制在容器框尺寸的边界之外。这类似于在容器上放置 overflow: hidden;,但有一些区别。

首先,容器会获得与layout容器下相同的处理方式:它成为一个具有自身堆叠上下文的包含块。因此,在paint容器内放置子元素时,会在放置方面尊重容器。如果我们复制上面的layout容器演示,但使用paint容器代替,结果将大致相同。区别在于,当应用容器时,紫色线条不会溢出容器,而是在容器的border-box处被裁剪。

paint容器的另一个有趣优势在于,如果浏览器能够检测到容器本身在视口中不可见,则它可以跳过该元素的后代的绘制计算。如果容器不在视口中或以某种方式被遮挡,则可以保证其后代也不可见。例如,考虑一个通常位于页面左侧屏幕外的导航菜单,当单击按钮时,它会滑入。当该菜单处于屏幕外的正常状态时,浏览器只需跳过尝试绘制其内容。

容器协同工作

这三种容器提供了影响浏览器执行的渲染计算部分的不同方法。size容器告诉浏览器,当其内容发生变化时,此容器不应导致页面上的位置偏移。layout容器告诉浏览器,此容器的后代不应导致其容器外部的元素发生布局更改,反之亦然。paint容器告诉浏览器,此容器的内容永远不会绘制在容器尺寸之外,并且如果容器被遮挡,则根本不要绘制内容。

由于这些都提供了不同的优化,因此将其中一些组合在一起是有意义的。规范实际上允许这样做。例如,我们可以将layoutpaint组合使用作为contain属性的值,如下所示

.el {
  contain: layout paint;
}

由于这是一件显而易见的事情,因此规范实际上提供了两个简写值

简写完整写法
contentlayout paint
strictlayout paint size

content值在具有许多动态元素的 Web 项目中最常使用,例如内容随时间或用户活动而变化的大量多个容器。

strict值对于具有已定义大小且即使内容发生变化也不会改变的容器很有用。一旦到位,它将保持预期的尺寸。一个简单的例子是包含第三方外部广告内容的 div,该内容具有行业定义的尺寸,并且在页面 DOM 方面与任何其他内容无关。

性能优势

本文的这一部分很难解释。问题在于,关于性能优势的视觉效果不多。大多数优势都是幕后优化,可以帮助浏览器决定在布局或绘制更改时该做什么。

为了展示contain属性的性能优势,我做了一个简单的示例,它更改了具有多个子元素的元素的font-size。这种更改通常会触发重新布局,这也会导致页面重新绘制。该示例涵盖了nonecontentstrictcontain值。

单选按钮更改应用于中心紫色框的contain属性的值。“更改字体大小”按钮将通过切换类来切换紫色框内容的font-size。不幸的是,此类更改也是重新布局的潜在触发器。如果您好奇,这里列出了 JavaScript 中的情况,然后是类似的CSS 列表,这些情况会触发此类布局和绘制计算。我敢打赌,比你想象的要多。

我完全不科学的过程是选择包含类型,在 Chrome 的开发者工具中开始性能记录,单击按钮,等待font-size更改,然后在一秒或两秒后停止记录。我针对每种包含类型进行了三次操作,以便能够比较多次记录。这种类型的比较数字都在几毫秒范围内,但差异足够大,可以了解其优势。在更真实的场景中,这些数字可能会大不相同。

但除了原始数字之外,还需要注意一些事项。

在查看记录时,我会在时间轴中找到相关区域并聚焦在那里,以选择涵盖更改的任务。然后,我会查看任务的事件日志以查看详细信息。记录的事件包括:重新计算样式、布局、更新图层树、绘制和合成图层。将所有这些时间相加,我们就得到了任务的总时间。

DevTools showing set time at 27.9 milliseconds which is the same as the total time to recalculate styles.
没有包含的事件日志。

需要注意的是,对于两种包含类型,绘制事件记录了两次。我稍后会详细说明。

DevTools showing set time at 13.8 milliseconds which is the same as the total time to recalculate styles.

完成手头任务

以下是三种包含类型的总时间,每种类型运行三次

包含运行 1运行 2运行 3平均值
none24 毫秒33.8 毫秒23.3 毫秒27.03 毫秒
content13.2 毫秒9 毫秒9.2 毫秒10.47 毫秒
strict5.6 毫秒18.9 毫秒8.5 毫秒11 毫秒

大部分时间都花在了布局上。在这些数字中这里和那里都有一些峰值,但请记住,这些都是不科学的轶事结果。事实上,strict包含的第二次运行的结果远高于其他两次运行;我只是保留了它,因为这种事情确实会发生在现实世界中。也许我当时正在听的音乐在那一轮中换了歌,谁知道呢。但你可以看到其他两次运行快得多。

因此,从这些数字中,您可以开始看到contain属性有助于浏览器更有效地渲染。现在想象一下我的一个小的更改被乘以对典型动态网页的 DOM 和样式所做的许多更改。

更有趣的是绘制事件的细节。

布局一次,绘制两次

坚持住。我保证它会有意义。

我将使用上面的演示作为以下描述的基础。如果您希望继续,请转到演示的完整版本并打开 DevTools。请注意,在运行性能工具后,您必须打开“帧”的详细信息而不是“主”时间轴才能看到我将要描述的内容。

Showing frame details open and main details closed in DevTools.
显示 DevTools 中打开的帧详细信息和关闭的主详细信息

我实际上是从“全页”版本中截取屏幕截图,因为 DevTools 在该版本中效果更好。也就是说,常规的“完整”版本应该会给出大致相同的想法。

在根本没有包含的任务的事件日志中,绘制事件仅触发了一次。通常,事件不会花费太长时间,范围从 0.2 毫秒到 3.6 毫秒。更深入的细节是它变得有趣的地方。在这些细节中,它指出绘制区域是整个页面。在事件日志中,如果您将鼠标悬停在绘制事件上,DevTools 甚至会突出显示已绘制的页面区域。在这种情况下,尺寸将是浏览器视口的大小。它还会记录绘制的图层根。

Showing DevTools paint calculation of 0.7 milliseconds.
绘制事件详细信息

请注意,图像左侧的页面区域突出显示,甚至在紫色框之外。右侧是绘制到屏幕的尺寸。在本例中,这大致是视口的大小。为了进行将来的比较,请注意#document作为图层根。

请记住,浏览器对某些元素具有图层概念,以帮助绘制。图层通常用于可能由于新的堆叠上下文而相互重叠的元素。一个例子是将position: relative;z-index: 1;应用于元素的方式将导致浏览器创建该元素作为新图层。contain属性具有相同的效果。

DevTools 中有一个名为“渲染”的部分,它提供了各种工具来查看浏览器如何渲染页面。选择名为“图层边框”的复选框时,我们可以根据包含看到不同的内容。当包含为 none 时,除了典型的静态网页图层之外,您应该看不到任何图层。选择contentstrict,您会看到紫色框转换为自己的图层,并且页面的其余图层相应地发生偏移。

没有包含的图层
有包含的图层

在图像中可能很难注意到,但在选择content包含后,紫色框成为自己的图层,并且页面在框后面发生了图层偏移。还要注意,在顶部的图像中,图层线在框的顶部延伸,而在第二个图像中,图层线在框的下方。

我之前提到过,contentstrict都会导致绘制触发两次。这是因为出于两个不同的原因执行了两个绘制过程。在我的演示中,第一个事件用于紫色框,第二个事件用于紫色框的内容。

通常,第一个事件将绘制紫色框并报告该框的尺寸作为事件的一部分。该框现在是它自己的图层,并享有适用的优势。

第二个事件用于框的内容,因为它们是滚动元素。正如规范所解释的那样;由于堆叠上下文是保证的,因此滚动元素可以绘制到单个 GPU 图层中。第二个事件中报告的尺寸更高,即滚动元素的高度。可能还会更窄以腾出空间用于滚动条。

使用内容包含的第一个绘制事件
使用内容包含的第二个绘制事件

请注意这两张图片右侧尺寸的差异。此外,这两个事件的图层根都是main.change,而不是上面看到的#document。紫色框是一个main元素,因此只绘制了该元素,而不是整个文档。您可以看到该框被高亮显示,而不是整个页面。

这样做的好处是,通常当滚动元素进入视野时,它们需要被绘制。包含在容器内的滚动元素已经被绘制,在进入视野时不需要再次绘制。因此,我们也获得了一些滚动优化。

同样,这可以在演示中看到。

回到“渲染”选项卡。这次,选择“滚动性能问题”选项。当包含设置为none时,Chrome 会用一个标有“滚动时重绘”的覆盖层覆盖紫色框。

DevTools 显示“滚动时重绘”,没有包含

如果您希望看到这种情况实时发生,请选中“绘制闪烁”选项。

请注意:如果屏幕上的闪烁颜色以某种方式对您造成问题,请考虑不要选中“绘制闪烁”选项。在我刚刚描述的示例中,页面上没有太多变化,但如果有人选中该选项并访问其他网站,则反应可能会有所不同。

启用绘制闪烁后,您应该看到一个绘制指示器,只要在紫色框内滚动,它就会覆盖框内的所有文本。现在将包含更改为contentstrict,然后再次滚动。在第一次初始绘制闪烁之后,它不应该再次出现,但滚动条在滚动时会显示绘制的迹象。

启用绘制闪烁并在没有包含的情况下滚动
启用绘制闪烁并在内容包含的情况下滚动

还要注意,两种包含形式的“滚动时重绘”覆盖层都消失了。在这种情况下,包含不仅为我们提供了一些绘制方面的性能提升,而且在滚动方面也提供了性能提升。

一个有趣的意外发现

当我使用上面的演示并了解绘制和滚动性能方面的工作原理时,我遇到一个有趣的问题。在一个测试中,我在页面中心有一个简单的框,但样式很少。它本质上是一个带有大量文本内容的滚动元素。我将content包含应用于容器元素,但没有看到上面描述的滚动性能优势。

该容器被标记为“滚动时重绘”覆盖层,绘制闪烁与不应用包含的情况相同,即使我确信content包含已应用于该容器。因此,我开始将我的简单测试与上面讨论的样式更丰富的版本进行比较。

我最终发现,如果容器的background-color是透明的,则包含滚动性能优势不会发生。

我运行了一个类似的性能测试,其中我将内容的font-size更改为触发重新布局和重绘。这两个测试的结果大致相同,唯一的区别是第一个测试具有透明的background-color,而第二个测试具有正确的背景颜色。从数字上看,后台计算仍然更有效;只有绘制事件不同。似乎在具有透明background-color的情况下,元素不会在绘制计算中成为自己的图层。

第一次测试运行在事件日志中只有一个绘制事件。第二次测试运行有两个绘制事件,这正是我期望的。如果没有该背景颜色,浏览器似乎决定跳过包含的图层方面。我甚至发现,通过使用与元素后面颜色相同的颜色来模拟透明度也能达到相同的效果。我的猜测是,如果容器的背景是透明的,那么它必须依赖于下面的任何内容,这使得无法将容器分离到它自己的绘制图层。

我制作了测试演示的另一个版本,它将容器元素的background-color从透明更改为与主体背景颜色相同的颜色。以下是使用 DevTools 中“渲染”面板的各种选项时显示差异的两个屏幕截图。

具有透明background-color的渲染面板

您可以看到已选中的复选框以及对容器的结果。即使应用了内容包含,该框也具有“滚动时重绘”以及显示滚动时绘制的绿色覆盖层。

具有background-color的渲染面板

在第二个图像中,您可以看到选择了相同的复选框,但对容器的结果不同。“滚动时重绘”覆盖层消失了,用于绘制的绿色覆盖层也消失了。您可以在滚动条上看到绘制覆盖层,以显示它处于活动状态。

结论:在应用包含以获得所有好处时,请确保为容器应用某种形式的背景颜色。

这是我用于测试的内容

页面底部

本文介绍了 CSS contain 属性及其值、好处和潜在的性能提升的基础知识。将此属性应用于 HTML 中的某些元素有一些非常好的好处;哪些元素需要应用此属性取决于您。至少,这就是我从收集到的信息,因为我不知道任何具体的指导。总体思路是将其应用于作为其他元素容器的元素,尤其是那些具有某种动态方面的元素。

一些可能的场景:CSS 网格的网格区域、包含第三方内容的元素以及基于用户交互具有动态内容的容器。在这些情况下使用该属性应该不会有任何害处,假设您不是试图包含一个实际上依赖于该包含之外的其他元素的元素。

浏览器支持非常强大。Safari 是目前唯一一个不支持的浏览器。您仍然可以使用该属性,因为如果浏览器不理解该属性或其值,它只会简单地跳过该代码而不会出错。

因此,请随时开始包含您的内容!