层模型
简介
对于大多数网页开发者来说,网页的基本模型是 DOM。呈现是一个通常比较模糊的过程,需要将网页的这种呈现内容转换成屏幕上的图片。近年来,现代浏览器改变了渲染的工作方式,以充分利用显卡:这通常被称为“硬件加速”。在谈论普通网页(即不是 Canvas2D 或 WebGL)时,该术语的真正含义是什么?本文介绍了支持在 Chrome 中对网页内容进行硬件加速渲染的基本模型。
高脂注意事项
这里我们要讨论的是 WebKit,更具体地说,就是谈论 WebKit 的 Chromium 端口。本文介绍了 Chrome 的实现详情,而不是网络平台功能的相关信息。网络平台和标准并未标准化这种级别的实现细节,因此,我们不保证本文中的任何内容也适用于其他浏览器,但了解内部知识仍然对高级调试和性能调整非常有用。
另请注意,整篇文章都在讨论 Chrome 渲染架构中一个快速变化的核心部分。本文尝试仅介绍基本情况,但不能保证在 6 个月后仍然适用。
请务必注意,Chrome 现在有两个不同的渲染路径:硬件加速路径和旧软件路径。截至撰写本文时,所有页面都将采用 Windows、ChromeOS 和 Chrome(Android 版)的硬件加速路径。在 Mac 和 Linux 上,只有需要为部分内容进行合成的网页会采用加速路径(请参阅下文,详细了解需要合成的内容),但很快所有网页也将采用加速路径。
最后,我们将深入了解渲染引擎,了解其对性能有重大影响的功能。当您尝试改进自己网站的性能时,了解层模型会很有帮助,但同时也很容易进行自己动手操作:层是有用的结构,但是创建很多层会给整个图形堆栈带来开销。想想自己被预先警告了!
从 DOM 到屏幕
图层简介
加载并解析页面后,系统会在浏览器中将其表示为许多 Web 开发者都熟悉的结构:DOM。不过,在呈现网页时,浏览器会呈现一系列不会直接提供给开发者的中间呈现。这些结构中最重要的是层。
在 Chrome 中,实际上有几种不同类型的层:负责 DOM 子树的 RenderLayer 和负责 RenderLayer 的子树的 GraphicsLayer。对于后者,我们对后者最感兴趣,因为 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>
<ph type="x-smartling-placeholder">
此页面只有一个图层。蓝色网格表示图块,您可以将图块视为图层的子单元,Chrome 使用它一次将大型图层的某些部分上传到 GPU。它们在这里并不是很重要。
图 2:一个元素位于各自的层中
<!doctype html>
<html>
<body>
<div style="transform: rotateY(30deg) rotateX(-30deg); width: 200px;">
I am a strange root.
</div>
</body>
</html>
<ph type="x-smartling-placeholder">
通过在旋转它的 <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>
每次点击输入元素时,旋转的元素会加宽 1px。这会导致对整个元素(在本例中为整个图层)进行重新布局和重绘。
查看正在绘制的内容的一个好方法是使用开发者工具中的“显示绘制矩形”工具,该工具位于开发者工具设置的“渲染”标题下。启用后,请注意,当用户点击动画元素和按钮时,该按钮都会闪烁红色。
绘制事件也会显示在开发者工具时间轴中。眼尖的读者可能会注意到有两个绘制事件:一个是图层,另一个是按钮本身。当按钮变为/从按下状态改变时,按钮会重新绘制。
请注意,Chrome 并不总是需要重新绘制整个层,而是会尽量明智地只重新绘制 DOM 中失效的部分。在本例中,我们修改的 DOM 元素为整个层的大小。但在许多其他情况下,层中会包含许多 DOM 元素。
下一个显而易见的问题是导致失效并强制重绘的原因。很难详尽地回答这个问题,因为有很多极端情况会强制失效。最常见的原因是操纵 CSS 样式或导致重新布局破坏了 DOM。Tony Gentilcore 有一篇精彩的博客文章介绍了导致重新布局的原因,Stoyan Stefanov 也发表了一篇对绘画进行了更详细的介绍(但结尾只写了绘画,而不是这种花哨的合成内容)。
确定是否会影响您正在做的工作的最佳方法是,使用开发者工具的 Timeline 和 Show Paint Rects 工具,看看您是否会在希望不重新绘制时进行重新绘制,然后尝试在重新布局/重新绘制之前确认 DOM 在哪里。如果绘制不可避免,但花费的时间似乎过长,请参阅 Eberhard Gräther 撰写的文章,其中介绍了开发者工具中的连续绘制模式。
融会贯通:DOM 到屏幕
那么,Chrome 如何将 DOM 转变为屏幕图片呢?从概念上讲,它具有以下特点:
- 获取 DOM 并将其拆分为层
- 将每个图层单独绘制到软件位图中
- 将它们作为纹理上传到 GPU
- 将各个层合成为最终的屏幕图片。
这一切都需要在 Chrome 首次生成网页框架时完成。但对于未来的帧,它可以采取一些捷径:
- 如果某些 CSS 属性发生变化,则无需重新绘制任何内容。Chrome 只能对已作为纹理放置在 GPU 上的现有图层进行重组,但这些图层的合成属性不同(例如,位置不同、不透明度不同等)。
- 如果某个图层的某些部分失效,系统会重新绘制该图层并重新上传。如果其内容保持不变,但其合成属性发生变化(例如,图片被转换或不透明度发生变化),Chrome 可以将其留在 GPU 上并重组以制作新帧。
现在应该很清楚,基于层的合成模型会对渲染性能产生深远的影响。当不需要绘制任何内容时,合成的开销相当低,因此在尝试调试渲染性能时,避免重新绘制层是一个很好的总体目标。有经验的开发者会查看上面的合成触发器列表,并意识到轻松强制创建层。但要注意只是盲目地创建它们,因为它们不是免费的:它们会占用系统 RAM 和 GPU 中的内存(在移动设备上尤为有限),并且它们会在逻辑中产生其他开销,用以跟踪哪些是可见的。实际上,如果许多图层很大,并且与之前没有的重叠很多,那么它们还会增加光栅化所需的时间,从而导致有时称为“过度绘制”。因此,请明智地利用您的知识!
暂无新信息流。请继续关注更多关于层模型的实际含义的文章。