图层模型
简介
对于大多数 Web 开发者而言,网页的基本模型是 DOM。渲染是一个通常不太清晰的过程,它将网页的这种表示转换为屏幕上的图片。近年来,为了充分利用显卡,现代浏览器改变了渲染方式:这通常被笼统地称为“硬件加速”。在讨论普通网页(即非 Canvas2D 或 WebGL)时,这个术语的真正含义是什么?本文介绍了 Chrome 中 Web 内容硬件加速渲染的基础模型。
大而肥的注意事项
我们在这里讨论的是 WebKit,更具体地说,是 WebKit 的 Chromium 移植版本。本文介绍了 Chrome 的实现细节,而不是 Web 平台功能。Web 平台和标准未对此级别的实现细节进行编码,因此无法保证本文中的任何内容都适用于其他浏览器,但对内部机制的了解对于高级调试和性能调整还是很有帮助的。
另请注意,本文将讨论 Chrome 渲染架构中变化非常快的一项核心功能。本文仅会尝试介绍不太可能发生变化的内容,但无法保证这些内容在六个月后仍然适用。
请务必注意,Chrome 已经有段时间采用了两种不同的渲染路径:硬件加速路径和旧版软件路径。在撰写本文时,所有网页在 Windows、ChromeOS 和 Android 版 Chrome 上均采用硬件加速路径。在 Mac 和 Linux 上,只有需要对部分内容进行合成的网页才会采用加速路径(请参阅下文,详细了解哪些内容需要合成),但很快所有网页也将采用加速路径。
最后,我们将深入探究渲染引擎的内部机制,了解对性能有重大影响的功能。在尝试提升您自己网站的性能时,了解图层模型可能会有所帮助,但也容易自食其果:图层是实用的构造,但创建大量图层可能会在整个图形堆栈中引入开销。请注意!
从 DOM 到屏幕
图层简介
网页加载并解析后,会在浏览器中以许多 Web 开发者熟悉的结构表示:DOM。不过,在呈现网页时,浏览器会使用一系列中间表示法,这些表示法不会直接向开发者公开。其中最重要的结构是层。
在 Chrome 中,实际上有几种不同类型的层:RenderLayer(负责 DOM 的子树)和 GraphicsLayer(负责 RenderLayer 的子树)。后者对我们来说最有趣,因为 GraphicsLayer 会作为纹理上传到 GPU。接下来,我将直接使用“层”一词来表示 GraphicsLayer。
关于 GPU 术语的简短说明:什么是纹理?可以将其视为从主内存(即 RAM)移至显存(即 GPU 上的 VRAM)的位图图片。在 GPU 上之后,您可以将其映射到网格几何图形。在视频游戏或 CAD 程序中,此技术用于为 3D 骨架模型添加“皮肤”。Chrome 使用纹理将网页内容分块传送到 GPU。通过将纹理应用于非常简单的矩形网格,可以以低成本将纹理映射到不同的位置和转换。这就是 3D CSS 的运作方式,它也非常适合快速滚动,但我们稍后会详细介绍这两点。
我们来看几个示例,以便说明图层概念。
在 Chrome 中研究图层时,开发者工具中“渲染”标题下的“显示合成图层边框”标志非常有用,您可以通过设置(即小齿轮图标)来使用该标志。它会非常简单地突出显示层在屏幕上的位置。我们来开启它。这些屏幕截图和示例均来自本文撰写时最新的 Chrome Canary(Chrome 27)。
图 1:单层页面
<!doctype html>
<html>
<body>
<div>I am a strange root.</div>
</body>
</html>
此页面只有一层。蓝色网格代表功能块,您可以将其视为图层的子单元,Chrome 会使用这些子单元一次将大型图层的部分内容上传到 GPU。它们在这里并不是很重要。
图 2:位于自己图层中的元素
<!doctype html>
<html>
<body>
<div style="transform: rotateY(30deg) rotateX(-30deg); width: 200px;">
I am a strange root.
</div>
</body>
</html>
通过在用于旋转 <div>
的元素上设置 3D CSS 属性,我们可以看到元素获得自己的图层后的效果:请注意橙色边框,它在此视图中勾勒出了一个图层。
图层创建条件
还有哪些内容会获得自己的图层?Chrome 在此处使用的启发词语会随着时间的推移而不断演变,但目前,以下任何情况都会触发层的创建:
- 3D 或透视转换 CSS 属性
- 使用加速视频解码的
<video>
元素 - 具有 3D (WebGL) 上下文或加速 2D 上下文的
<canvas>
元素 - 复合插件(例如 Flash)
- 具有 CSS 动画不透明度或使用动画转换的元素
- 具有加速 CSS 过滤器的元素
- 元素具有具有合成层的后代(换句话说,如果元素具有位于其自身层中的子元素)
- 元素具有 z-index 较低且具有合成层的兄弟元素(换句话说,它会在合成层上方呈现)
实际影响:动画
我们还可以移动图层,这对于动画非常有用。
图 3:动画层
<!doctype html>
<html>
<head>
<style>
div {
animation-duration: 5s;
animation-name: slide;
animation-iteration-count: infinite;
animation-direction: alternate;
width: 200px;
height: 200px;
margin: 100px;
background-color: gray;
}
@keyframes slide {
from {
transform: rotate(0deg);
}
to {
transform: rotate(120deg);
}
}
</style>
</head>
<body>
<div>I am a strange root.</div>
</body>
</html>
如前所述,层对于移动静态 Web 内容非常有用。在基本情况下,Chrome 会先将图层的内容绘制到软件位图中,然后再将其作为纹理上传到 GPU。如果该内容日后不会发生变化,则无需重新绘制。这是好事:重新绘制需要时间,而这些时间可以用于执行其他操作(例如运行 JavaScript),如果绘制时间过长,会导致动画卡顿或延迟。
例如,请参阅开发者工具时间轴的此视图:在此图层来回旋转时,没有绘制操作。
无效!重新绘制
但是,如果图层的内容发生变化,则必须重新绘制。
图 4:重新绘制图层
<!doctype html>
<html>
<head>
<style>
div {
animation-duration: 5s;
animation-name: slide;
animation-iteration-count: infinite;
animation-direction: alternate;
width: 200px;
height: 200px;
margin: 100px;
background-color: gray;
}
@keyframes slide {
from {
transform: rotate(0deg);
}
to {
transform: rotate(120deg);
}
}
</style>
</head>
<body>
<div id="foo">I am a strange root.</div>
<input id="paint" type="button" value="repaint">
<script>
var w = 200;
document.getElementById('paint').onclick = function() {
document.getElementById('foo').style.width = (w++) + 'px';
}
</script>
</body>
</html>
每次点击输入元素时,旋转元素都会宽出 1 像素。这会导致重新布局和重新绘制整个元素(在本例中为整个图层)。
如需查看正在绘制的内容,不妨使用 DevTools 中的“show paint rects”工具(也位于 DevTools 设置的“Rendering”标题下)。开启此属性后,请注意,点击按钮时,动画元素和按钮都会闪烁红色。
绘制事件也会显示在开发者工具时间轴中。目光敏锐的读者可能会注意到,其中有两个绘制事件:一个用于图层,另一个用于按钮本身,当按钮从按下状态切换到/从按下状态时,系统会重新绘制该按钮。
请注意,Chrome 并不总是需要重新绘制整个图层,而是会智能地仅重新绘制失效的 DOM 部分。在本例中,我们修改的 DOM 元素是整个图层的尺寸。但在许多其他情况下,一个图层中会包含许多 DOM 元素。
显而易见,下一个问题是导致失效并强制重新绘制的原因。很难全面回答这个问题,因为有许多边缘情况可能会强制失效。最常见的原因是通过操控 CSS 样式或导致重新布局而使 DOM 变脏。Tony Gentilcore 有一篇很棒的博文,介绍了导致重新布局的原因,而 Stoyan Stefanov 有一篇文章,更详细地介绍了绘制(但它只介绍了绘制,而没有介绍这些复杂的合成内容)。
若要确定它是否会影响您正在处理的内容,最好的方法是使用开发者工具时间轴和“显示绘制矩形”工具,看看您是否在不需要时重新绘制,然后尝试确定在重新布局/重新绘制之前哪些地方会污染 DOM。如果绘制不可避免,但似乎花费的时间过长,请参阅 Eberhard Gräther 的文章,了解开发者工具中的连续绘制模式。
整合:DOM 到屏幕
那么,Chrome 如何将 DOM 转换为屏幕图片?从概念上讲,它:
- 获取 DOM 并将其拆分为多个图层
- 将这些图层中的每个图层单独绘制到软件位图中
- 将其作为纹理上传到 GPU
- 将各种图层合并到最终的屏幕图片中。
所有这些都需要在 Chrome 首次生成网页帧时完成。但之后,它可以针对未来的帧使用一些快捷方式:
- 如果某些 CSS 属性发生变化,则无需重新绘制任何内容。Chrome 只需将已位于 GPU 上的现有图层重新组合为纹理,但具有不同的合成属性(例如,位于不同的位置、具有不同的不透明度等)。
- 如果图层的某个部分无效,系统会重新绘制并重新上传该部分。如果其内容保持不变,但其合成属性发生变化(例如,发生平移或不透明度发生变化),Chrome 可以将其保留在 GPU 上并重新合成以创建新帧。
现在应该已经很清楚了,基于层的合成模型对渲染性能有深远的影响。当不需要绘制任何内容时,合成成本相对较低,因此在尝试调试渲染性能时,避免重绘层是一个很好的总体目标。精明的开发者会查看上面的合成触发器列表,并意识到可以轻松强制创建图层。但请注意,不要盲目创建它们,因为它们并非免费的:它们会占用系统 RAM 和 GPU 中的内存(在移动设备上尤为有限),并且如果有大量这样的对象,可能会在跟踪哪些对象可见的逻辑中引入其他开销。如果图层很多且与之前没有重叠的图层有大量重叠,实际上也可能会增加光栅化所花的时间,从而导致有时被称为“过度绘制”的情况。因此,请明智地运用您的知识!
就先这样吧。敬请关注我们后续几篇文章,了解层模型的实际应用。