Chrome 中的加速呈现

层模型

Tom Wiltzius
Tom Wiltzius

简介

对于大多数网络开发者来说,网页的基本模型是 DOM。渲染是将网页的这种表现转换成屏幕上的图片的过程通常较为模糊。近年来,现代浏览器已经改变了渲染工作方式,以充分利用显卡:这通常被模糊地称为“硬件加速”。当谈论普通网页(即不是 Canvas2D 或 WebGL)时,该术语到底是什么意思?本文介绍了在 Chrome 中对网页内容进行硬件加速渲染的基础模型。

关于脂肪的重大警示

这里我们讨论的是 WebKit,更具体地说,我们讨论的是 WebKit 的 Chromium 端口。本文介绍的是 Chrome 的实现细节,而非网络平台功能。网络平台和标准并不整理这种级别的实现细节,因此不保证本文中的任何内容也适用于其他浏览器,但了解内部机制对于高级调试和性能优化仍然很有用。

另请注意,整篇文章都在讨论 Chrome 呈现架构中一个变化极快的核心部分。本文尝试仅讨论那些不太可能改变的方面,但不保证这些变化在六个月内依然适用。

请务必注意,Chrome 已有一段时间采用了两种不同的渲染路径:硬件加速路径和旧版软件路径。撰写本文时,所有页面都将采用 Windows、ChromeOS 和 Android 版 Chrome 的硬件加速路径。在 Mac 和 Linux 上,只有需要对部分内容进行合成的网页会沿着加速路径前进(请参阅下文,详细了解需要合成的内容),但很快所有网页也会沿加速路径前进。

最后,我们将深入了解渲染引擎的背后,了解它对性能有重大影响的功能。当您尝试改进自己网站的性能时,了解层模型可能很有帮助,但白忙也很容易:层是有用的结构,但创建大量层会给整个图形堆栈带来开销。你们要预先预警!

从 DOM 到屏幕

图层简介

页面加载并解析后,会在浏览器中以许多网络开发者都熟悉的结构表示:DOM。不过,在呈现网页时,浏览器会采用一系列不会直接提供给开发者的中间表示法。这些结构中最重要的是层。

Chrome 中实际上有几种不同类型的层:负责 DOM 子树的 RenderLayer 和负责 RenderLayer 的子树的 GraphicsLayer。后者对我们这里来说最有趣,因为 GraphicsLayer 会作为纹理上传到 GPU。从现在开始,我要说“layer”是指 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>
旋转图层的渲染边框的屏幕截图
旋转图层渲染边框的屏幕截图

通过将 3D CSS 属性放置在用于旋转该元素的 <div> 上,我们可以看到元素拥有自己的图层时的外观:请注意橙色边框,它用于勾勒此视图中的图层。

图层创建条件

还有什么有自己的层?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>

如前所述,层对于在静态网页内容之间移动非常有用。在基本的情况下,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 像素。这会导致对整个元素(在本例中为整个图层)进行重新布局和重新绘制。

使用开发者工具中的“show paint rects”工具(也是在开发者工具设置的“Rendering”(渲染)标题下)查看绘制内容的一个好办法。启用后,请注意,在用户点击按钮时,动画元素和按钮都会闪烁红色。

“显示绘制矩形”复选框的屏幕截图
“显示绘制矩形”复选框的屏幕截图

绘制事件也会显示在开发者工具时间轴中。敏锐的读者可能会注意到有两个绘制事件:一个针对图层,一个针对按钮本身,当按钮改变为按下状态或从按下状态发生变化时重新绘制。

开发者工具时间轴重绘图层的屏幕截图
开发者工具时间轴重绘图层的屏幕截图

请注意,Chrome 并非总是需要重新绘制整个图层,它会尝试智能地仅重新绘制已失效的 DOM 部分。在本示例中,我们修改的 DOM 元素为整个层的大小。但在许多其他情况下,一个层中会有大量 DOM 元素。

显而易见的下一个问题是导致失效并强制重新绘制的原因。这个问题很难回答详尽,因为有很多极端情况可能会强制失效。最常见的原因是通过操纵 CSS 样式或导致重新布局,使 DOM 弄脏。Tony Gentilcore 关于重新布局的原因的精彩博文是一篇非常棒的博文,Stoyan Stefanov 还撰写了一篇详细介绍绘画的文章(但本文的结尾只是绘画,而不是这种精美的合成作品)。

若要确定它是否会影响您正在做的工作,最好的方法是使用开发者工具时间轴和“显示绘制矩形”工具,看看您是否在按希望重新绘制时重新绘制,然后尝试确定在重新布局/重绘之前弄脏了 DOM 的位置。如果绘制不可避免,但耗时过长,请参阅 Eberhard Gräther 的文章,介绍了开发者工具中的连续绘制模式。

整合:DOM 到屏幕

那么 Chrome 是如何将 DOM 转换成屏幕图像的呢?从概念上讲,它:

  1. 获取 DOM 并将其拆分为多个层
  2. 将每个层分别绘制成软件位图
  3. 将它们作为纹理上传到 GPU
  4. 将各个层合成为最终的屏幕图像。

这一切都需要在 Chrome 首次生成网页框架时实现。不过,它可为未来的帧采用一些快捷方式:

  1. 如果某些 CSS 属性发生变化,则无需重绘任何内容。Chrome 可以直接将已位于 GPU 上且具有不同合成属性(例如处于不同位置、具有不同的不透明度等)的现有图层合成为纹理。
  2. 如果图层的某些部分失效,系统会重新绘制该部分并重新上传。如果其内容保持不变,但合成属性发生变化(例如转换或其透明度发生变化),则 Chrome 可将其保留在 GPU 上并进行重组,以创建新的帧。

现在应该很清楚,基于层的合成模型对渲染性能有着深远的影响。当不需要绘制任何内容时,合成的成本相对较低,因此在尝试调试渲染性能时,避免重绘层是一个总体目标。有经验的开发者会看看上面的合成触发器列表,意识到可以轻松地强制创建层。但请注意,只是盲目创建它们是不自由的:它们会占用系统 RAM 和 GPU 的内存(在移动设备上尤其有限),如果它们太多,会在逻辑跟踪哪些可见元素时引入其他开销。实际上,许多图层如果图层较大且与之前没有重叠的位置存在很多重叠,还会导致光栅化所用时间增加,从而导致有时所谓的“过度绘制”。因此,请明智地运用自己的知识!

以上就是所有代金券。请继续关注更多有关层模型实际影响的文章。

其他资源