开头的废话
最近在研究浏览器的合成,这部分内容是浏览器的实现细节,在Web标准没有定义这部分细节,因此,原始资料并不太好找。除了Chromium的设计文档,本文相对来说比较易理解,故将其翻译出来。除了这篇文章,最近还会对另外两篇相关内容的文章进行翻译,结合这些资料再做自己的总结。
本文作者是Tom Wiltzius,发布于2013年3月11日。尽管文中诸多细节内容早已过时,但并不妨碍对核心思想作一些初步的了解。感兴趣的话,也可以到英文原文中进行对照。
简介
对于大多数Web开发人员来说,网页的基本模型是DOM。而渲染通常是晦涩难懂的过程,这个过程将页面的这种表示形式转换为屏幕上的图片。近年来,现代浏览器已经改变了渲染的方式,从而利用显卡:这通常被模糊地被称为“硬件加速”。当谈到普通的网页时(不是 Canvas2D或WebGL),这个术语的真正含义是什么?本文介绍了Chrome支持Web内容硬件加速渲染的基本模型。
重大警告
我们在这里谈论WebKit,更具体地说,我们谈论的是WebKit的Chromium端。本文覆盖的是Chrome的实现细节,而不是Web平台功能。Web平台和标准并未对这一级别的实现细节进行整理,因此无法保证本文中的任何内容都适用于其他浏览器,但底层知识对于高级调试和性能调整仍然有用。
另外,请注意,整篇文章都在讨论Chrome渲染架构的核心部分,这部分变化速度非常快。本文试图仅涵盖不太可能更改的内容,但不能保证在六个月内它仍然都适用。
重要的是要理解,在一段时间以来,Chrome有两种不同的渲染途径:硬件加速途径和较旧的软件渲染途径。在撰写本文时,所有页面都在Windows、ChromeOS和Chrome for Android上采用硬件加速的途径。在Mac和Linux上,只有需要对某些内容进行合成的页面才会采用加速途径(更多有关需要合成的信息,请参见下文),但很快所有页面也将采用加速途径。
最后,我们正在窥探渲染引擎的底层,并查看对性能有重大影响的特性。在尝试提高您自己网站的性能时,了解图层模型可能会有所帮助,但也很容易使自己陷入困境:图层是非常有用的结构,但创建大量图层会在整个图形堆栈中引入开销。请自视事先的警告!
从DOM到屏幕
介绍图层
一旦页面被加载和解析,它就在浏览器中表现为许多Web开发人员熟悉的结构:DOM。然而,在渲染页面时,浏览器有一系列不直接暴露给开发人员的中间形式。这些结构中最重要的是图层。
在Chrome中,实际上有几种不同类型的层:RenderLayers负责DOM子树,GraphicsLayers负责RenderLayers子树。后者在这里对我们来说最有趣,因为GraphicsLayers被作为纹理上传到GPU。从这里开始,我说的“图层”就表示GraphicsLayer。
快速了解GPU术语:什么是纹理?可以将其视为从内存(RAM)移动到显存(即GPU上的VRAM)的位图。一旦它在GPU上,您可以将其映射到网格几何体——在视频游戏或CAD程序中,这种技术用于为3D骨骼模型提供“皮肤”。Chrome通过纹理将大量网页内容放到GPU上。通过应用纹理到一个非常简单的矩形网格,纹理可以被廉价地映射到不同的位置和转换。这就是3D CSS的工作原理,它也非常适合快速滚动——但稍后会详细介绍这两者。
让我们看几个例子来说明图层的概念。
在Chrome中研究图层时,一个非常有用的工具是在开发工具的设置中,“rendering”标题下的“show composited layer borders”。它简单地高亮了图层在屏幕上的位置。我们可以打开它。这些屏幕截图和示例均取自撰写本文时最新的Chrome Canary,Chrome 27。
Figure 1:单层页面
<!doctype html>
<html>
<body>
<div>I am a strange root.</div>
</body>
</html>
这个页面只有一个图层。蓝色网格表示图块,您可以将其视为图层的子单元,Chrome利用图块每次仅上传大图层的一部分到GPU。但它们在这里并不重要。
Figure 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)
- 带有
opacity动画或transform动画的元素 - 带有加速CSS
filters的元素 - 元素有一个具有合成层的后代(换句话说,如果该元素存在子元素在独立的图层上)
- 元素有一个
z-index较低的兄弟元素,这个兄弟元素具有合成层(换句话说,它呈现在合成层的顶部)
实际意义:动画
我们也可以移动图层,这使得它们对动画非常有用。
Figure 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,如果绘制时间过长,则会导致动画出现阻塞或延迟。
例如,参见Dev Tools timeline的视图:当该层来回旋转时,没有绘制操作。
失效!重绘
但是如果图层的内容发生变化,就必须重新绘制。
Figure 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。这会导致整个元素的重新布局和重新绘制,而且在这种情况下是整个层重新布局和重新绘制。
查看正在绘制的内容的方法是,使用开发工具中的“show paint rects”工具,也在开发工具设置的“Rendering”标题下。打开它后,请注意,当单击按钮时,动画元素和按钮都闪烁红色。
绘制事件也显示在开发工具时间线中。细心的读者可能会注意到那里有两个绘制事件:一个用于图层,一个用于按钮本身,当按钮更改其按下状态时会重新绘制。
请注意,Chrome并不总是需要重新绘制整个图层,它会尝试智能地仅重新绘制已失效的部分DOM。在这种情况下,我们修改的DOM元素是整个图层的大小。但在许多其他情况下,一个图层会有很多DOM元素。
另一个明显的问题是什么会导致失效并强制重绘。这很难详尽地回答,因为有很多边缘情况可以强制失效。最常见的原因是通过操纵CSS样式或导致重新布局弄脏DOM。Tony Gentilcore有一篇关于重新布局原因的精彩博客文章,Stoyan Stefanov有一篇文章更详细地介绍了Paint(但他仅讲述了Paint,而不是这种复杂的合成过程)。
想弄清这是否影响您正在处理的内容,最好的方法是使用开发者工具“Timeline”和“Show Paint Rects tools”来查看,在您不希望重新绘制时是否存在重新绘制的情况,然后尝试确定在重新布局/重绘之前,您弄脏了哪里的DOM。如果绘画是不可避免的,但似乎花费了不合理的时间,请查看Eberhard Gräther的文章——在开发者工具中的连续绘画模式。
放在一起:DOM到屏幕
那么Chrome是如何将DOM变成屏幕图像的呢?从概念上讲,它:
- 获取
DOM并将其拆分为图层 - 将每个图层独立地绘制到软件位图中
- 将它们作为纹理上传到GPU
- 将各个图层合成到最终的屏幕图像中。
这一切都会在Chrome第一次生成页面帧时发生。但是它可以为未来的帧走一些捷径:
- 如果某些CSS属性发生更改,则无需重新绘制任何内容。Chrome可以将已经位于GPU上的现有图层作为纹理重新合成,但具有不同的合成属性(例如,在不同的位置,具有不同的不透明度等)。
- 如果图层部分无效,则会重新绘制并重新上传。如果它的内容保持不变,但它的合成属性发生了变化(例如它被移动或不透明度发生了变化),Chrome可以将它留在GPU上并重新合成以制作一个新帧。
现在应该清楚了,基于层的合成模型对渲染性能有着深远的影响。当不需要绘制任何内容时,合成相对便宜,因此在尝试调试渲染性能时,避免重新绘制图层是一个很好的总体目标。精明的开发人员将查看上面的触发合成的列表,并意识到可以轻松地强制创建图层。但要小心盲目地创建它们,因为它们不是没有开销的:它们占用系统RAM和GPU上的内存(特别是在移动设备上受限),并且有很多图层时,在逻辑中来跟踪哪些图层可见,还会引入其他开销。如果图层很大,并且在以前没有重叠的地方重叠很多图层,实际上也会增加图层光栅化所花费的时间,从而导致有时被称为“过度绘制”。
目前为止就这样了。请继续关注更多层模型实际意义的文章。