浏览器层合成与页面渲染优化

做者:黄浩群css

一个 CSS 属性引起的血案

Web 页面性能是前端开发特别须要关注的重点,评判前端 Web 页面性能的指标有不少,页面的流畅度是其中的一种,如何让页面变得 “柔顺丝滑”,要讨论起来可就是个至关有料的话题了。以前开发移动端 H5 页面的时候,就遇到过一个有趣的性能问题 —— 某个卖场页面在 IOS 手机上出现了严重的卡顿,但在安卓机型下却表现得十分流畅。概括一下在 iPhoneX 上测试的具体表现:html

  • 页面加载时存在明显的延迟,但经过代理抓到的网络请求耗时并不比 Android 的高;前端

  • 页面滚动时会出现短暂的局部白屏,即丢帧。node

根据这些表征状况不难推断出,应该是有什么东西在疯狂占用 CPU,卡住了渲染进程。git

然而具体是什么东西,要问我我也并不知道。对于这种无法经过断点定位到的问题,恐怕只有用上祖师爷亲传的 “代码二分法” 才能制服得了了。一番艰苦排查以后,问题的根源终于聚焦到了下边这行 CSS 代码上:github

filter: blur(100px);
复制代码

这行 CSS 代码用于实现一个高斯模糊,来构造一个优惠券模块的底部阴影。因为活动配置了多个优惠券,致使页面里存在多个设置了这个属性的 div 元素,而 IOS 手机的浏览器彷佛对这个属性的渲染十分吃力(然而为什么吃力的缘由不得而知),进而致使渲染进程的 CPU 占用率太高,最终形成卡顿。canvas

哦?CPU 忙不过来了?好办嘛!我给优惠券模块又加了这样一行代码,而后问题迎刃而解 ......浏览器

will-change: transform;
复制代码

你没看错,我也没写少,确实就是靠一行代码解决的。性能优化

认识它的人可能已经看出来了,大体原理其实很简单,这行代码可以开启 GPU 加速页面渲染,从而大大下降了 CPU 的负载压力,达到优化页面渲染性能的目的,不了解 CSS 硬件加速的能够看看这篇文章 Increase Your Site’s Performance with Hardware-Accelerated CSS微信

问题解决了,可是真的就这么完事了吗?本着 “拔树寻根” 的伟大原则,我把这个东西好好地研究了一番,才发现 GPU 加速其实没那么简单。

浏览器渲染流程

在具体讨论原理以前,咱们须要了解一下浏览器渲染流程的一些基本概念。浏览器渲染流程是个老生常谈的话题了,对于 “浏览器如何呈现一个页面的内容” 的这类问题,很多人均可以讲出一个相对完整的过程,从网络请求到浏览器解析,能够具体到不少的细节。除去网络资源获取的步骤,咱们理解的 Web 页面的展现,通常能够分为 构建 DOM 树构建渲染树布局绘制渲染层合成 几个步骤。

  • 构建 DOM 树:浏览器将 HTML 解析成树形结构的 DOM 树,通常来讲,这个过程发生在页面初次加载,或页面 JavaScript 修改了节点结构的时候。

  • 构建渲染树:浏览器将 CSS 解析成树形结构的 CSSOM 树,再和 DOM 树合并成渲染树。

  • 布局(Layout):浏览器根据渲染树所体现的节点、各个节点的CSS定义以及它们的从属关系,计算出每一个节点在屏幕中的位置。Web 页面中元素的布局是相对的,在页面元素位置、大小发生变化,每每会致使其余节点联动,须要从新计算布局,这时候的布局过程通常被称为回流(Reflow)。

  • 绘制(Paint):遍历渲染树,调用渲染器的 paint() 方法在屏幕上绘制出节点内容,本质上是一个像素填充的过程。这个过程也出现于回流或一些不影响布局的 CSS 修改引发的屏幕局部重画,这时候它被称为重绘(Repaint)。实际上,绘制过程是在多个层上完成的,这些层咱们称为渲染层(RenderLayer)。

  • 渲染层合成(Composite):多个绘制后的渲染层按照恰当的重叠顺序进行合并,然后生成位图,最终经过显卡展现到屏幕上。

这是一个基本的浏览器从解析到绘制一个 Web 页面的过程,跟上边页面卡顿问题的解决方法相关的,主要是最后一个环节 —— 渲染层合成。

渲染层合成

1、什么是渲染层合成

在 DOM 树中每一个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会造成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite),从而正确处理透明元素和重叠元素的显示。

这个模型相似于 Photoshop 的图层模型,在 Photoshop 中,每一个设计元素都是一个独立的图层,多个图层以恰当的顺序在 z 轴空间上叠加,最终构成一个完整的设计图。

对于有位置重叠的元素的页面,这个过程尤为重要,由于一旦图层的合并顺序出错,将会致使元素显示异常。

2、浏览器的渲染原理

从浏览器的渲染过程当中咱们知道,页面 HTML 会被解析成 DOM 树,每一个 HTML 元素对应了树结构上的一个 node 节点。而从 DOM 树转化到一个个的渲染层,并最终执行合并、绘制的过程,中间其实还存在一些过渡的数据结构,它们记录了 DOM 树到屏幕图形的转化原理,其本质也就是树结构到层结构的演化。

一、渲染对象(RenderObject)

一个 DOM 节点对应了一个渲染对象,渲染对象依然维持着 DOM 树的树形结构。一个渲染对象知道如何绘制一个 DOM 节点的内容,它经过向一个绘图上下文(GraphicsContext)发出必要的绘制调用来绘制 DOM 节点。

二、渲染层(RenderLayer)

这是浏览器渲染期间构建的第一个层模型,处于相同坐标空间(z轴空间)的渲染对象,都将归并到同一个渲染层中,所以根据层叠上下文,不一样坐标空间的的渲染对象将造成多个渲染层,以体现它们的层叠关系。因此,对于知足造成层叠上下文条件的渲染对象,浏览器会自动为其建立新的渲染层。可以致使浏览器为其建立新的渲染层的,包括如下几类常见的状况:

  • 根元素 document

  • 有明确的定位属性(relative、fixed、sticky、absolute)

  • opacity < 1

  • 有 CSS fliter 属性

  • 有 CSS mask 属性

  • 有 CSS mix-blend-mode 属性且值不为 normal

  • 有 CSS transform 属性且值不为 none

  • backface-visibility 属性为 hidden

  • 有 CSS reflection 属性

  • 有 CSS column-count 属性且值不为 auto或者有 CSS column-width 属性且值不为 auto

  • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画

  • overflow 不为 visible

DOM 节点和渲染对象是一一对应的,知足以上条件的渲染对象就能拥有独立的渲染层。固然这里的独立是不彻底准确的,并不表明着它们彻底独享了渲染层,因为不知足上述条件的渲染对象将会与其第一个拥有渲染层的父元素共用同一个渲染层,所以实际上,这些渲染对象会与它的部分子元素共用这个渲染层。

三、图形层(GraphicsLayer)

GraphicsLayer 实际上是一个负责生成最终准备呈现的内容图形的层模型,它拥有一个图形上下文(GraphicsContext),GraphicsContext 会负责输出该层的位图。存储在共享内存中的位图将做为纹理上传到 GPU,最后由 GPU 将多个位图进行合成,而后绘制到屏幕上,此时,咱们的页面也就展示到了屏幕上。

因此 GraphicsLayer 是一个重要的渲染载体和工具,但它并不直接处理渲染层,而是处理合成层。

四、合成层(CompositingLayer)

知足某些特殊条件的渲染层,会被浏览器自动提高为合成层。合成层拥有单独的 GraphicsLayer,而其余不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 的父层共用一个。

那么一个渲染层知足哪些特殊条件时,才能被提高为合成层呢?这里列举了一些常见的状况:

  • 3D transforms:translate3d、translateZ 等

  • video、canvas、iframe 等元素

  • 经过 Element.animate() 实现的 opacity 动画转换

  • 经过 СSS 动画实现的 opacity 动画转换

  • position: fixed

  • 具备 will-change 属性

  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition

所以,文首例子的解决方案,其实就是利用 will-change 属性,将 CPU 消耗高的渲染元素提高为一个新的合成层,才能开启 GPU 加速的,所以你也可使用 transform: translateZ(0) 来解决这个问题。

这里值得注意的是,很多人会将这些合成层的条件和渲染层产生的条件混淆,这两种条件发生在两个不一样的层处理环节,是彻底不同的。

另外,有些文章会把 CSS Filter 也列为影响 Composite 的因素之一,然而我验证后发现并无效果。

3、隐式合成

上边提到,知足某些显性的特殊条件时,渲染层会被浏览器提高为合成层。除此以外,在浏览器的 Composite 阶段,还存在一种隐式合成,部分渲染层在一些特定场景下,会被默认提高为合成层。

对于隐式合成,CSS GPU Animation 中是这么描述的:

This is called implicit compositing: One or more non-composited elements that should appear above a composited one in the stacking order are promoted to composite layers. (一个或多个非合成元素应出如今堆叠顺序上的合成元素之上,被提高到合成层。)

这句话可能很差理解,它实际上是在描述一个交叠问题(overlap)。举个例子说明一下:

  • 两个 absolute 定位的 div 在屏幕上交叠了,根据 z-index 的关系,其中一个 div 就会”盖在“了另一个上边。

  • 这个时候,若是处于下方的 div 被加上了 CSS 属性:transform: translateZ(0),就会被浏览器提高为合成层。提高后的合成层位于 Document 上方,假如没有隐式合成,本来应该处于上方的 div 就依然仍是跟 Document 共用一个 GraphicsLayer,层级反而降了,就出现了元素交叠关系错乱的问题。

  • 因此为了纠正错误的交叠顺序,浏览器必须让本来应该”盖在“它上边的渲染层也同时提高为合成层。

4、层爆炸和层压缩

一、层爆炸

从上边的研究中咱们能够发现,一些产生合成层的缘由太过于隐蔽了,尤为是隐式合成。在平时的开发过程当中,咱们不多会去关注层合成的问题,很容易就产生一些不在预期范围内的合成层,当这些不符合预期的合成层达到必定量级时,就会变成层爆炸。

层爆炸会占用 GPU 和大量的内存资源,严重损耗页面性能,所以盲目地使用 GPU 加速,结果有可能会是拔苗助长。CSS3硬件加速也有坑 这篇文章提供了一个颇有趣的 DEMO,这个 DEMO 页面中包含了一个 h1 标题,它对 transform 应用了 animation 动画,进而致使被放到了合成层中渲染。因为 animation transform 的特殊性(动态交叠不肯定),隐式合成在不须要交叠的状况下也能发生,就致使了页面中全部 z-index 高于它的节点所对应的渲染层所有提高为合成层,最终让这个页面整整产生了几千个合成层。

消除隐式合成就是要消除元素交叠,拿这个 DEMO 来讲,咱们只须要给 h1 标题的 z-index 属性设置一个较高的数值,就能让它高于页面中其余元素,天然也就没有合成层提高的必要了。点击 DEMO 中的复选按钮就能够给 h1 标题加上一个较大的 z-index,先后效果对比十分明显。

二、层压缩

固然了,面对这种问题,浏览器也有相应的应对策略,若是多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止因为重叠缘由致使可能出现的“层爆炸”。这句话很差理解,具体能够看看这个例子:

  • 仍是以前的模型,只不过此次不一样的是,有四个 absolute 定位的 div 在屏幕内发生了交叠。此时处于最下方的 div 在加上了 CSS 属性 transform: translateZ(0) 后被浏览器提高为合成层,若是按照隐式合成的原理,盖在它上边的 div 会提高为一个新的合成层,第三个 div 又盖在了第二个上,天然也会被提高为合成层,第四个也同理。这样一来,岂不是就会产生四个合成层了?

  • 然而事实并非这样的,浏览器的层压缩机制,会将隐式合成的多个渲染层压缩到同一个 GraphicsLayer 中进行渲染,也就是说,上方的三个 div 最终会处于同一个合成层中,这就是浏览器的层压缩。

固然了,浏览器的自动层压缩并非万能的,有不少特定状况下,浏览器是没法进行层压缩的,无线性能优化:Composite 这篇文章列举了许多详细的场景。

基于层合成的页面渲染优化

1、层合成的得与失

层合成是一个相对复杂的浏览器特性,为何咱们须要关注这么底层又难理解的东西呢?那是由于渲染层提高为合成层以后,会给咱们带来很多好处:

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快得多;

  • 当须要 repaint 时,只须要 repaint 自己,不会影响到其余的层;

  • 元素提高为合成层后,transform 和 opacity 才不会触发 repaint,若是不是合成层,则其依然会触发 repaint。

固然了,利弊是相对和共存的,层合成也存在一些缺点,这不少时候也成为了咱们网页性能问题的根源所在:

  • 绘制的图层必须传输到 GPU,这些层的数量和大小达到必定量级后,可能会致使传输很是慢,进而致使一些低端和中端设备上出现闪烁;

  • 隐式合成容易产生过量的合成层,每一个合成层都占用额外的内存,而内存是移动设备上的宝贵资源,过多使用内存可能会致使浏览器崩溃,让性能优化拔苗助长。

2、Chrome Devtools 如何查看合成层

层合成的特性给咱们提供了一个利用终端硬件能力来优化页面性能的方式,对于一些重交互、重动画的页面,合理地利用层合成可让页面的渲染效率获得极大提高,改善交互体验。而咱们须要关注的是如何规避层合成对页面形成的负面影响,或者换个说法来说,更多时候是如何权衡利害,合理组织页面的合成层,这就要求咱们事先要对页面的层合成状况有一个详细的了解。Chrome Devtools 给咱们提供了一些工具,能够方便的查看页面的合成层状况。

首先是看看页面的渲染状况,以一个栏目页为例,点击 More tools -> Rendering,选择 Layer borders,你就能看到页面中的合成层都带上了黄色边框。

这还不够,咱们还须要更加详尽的层合成状况,点击 More tools -> Layers,你能够看到像这样的一个视图:

左侧列出了全部提高为独立合成层的元素,右侧则是一个总体合成层边界视图,以及选定合成层的详细状况,包括如下几个比较关键的信息:

  • Size:合成层的大小,其实也就是对应元素的尺寸;
  • Compositing Reasons:造成复合层缘由,这是最关键的,也是咱们分析问题的突破口,好比图中的合成层产生的缘由就是交叠问题;
  • Memory estimate:内存占用估算;
  • Paint count:绘制次数;
  • Slow scroll regions:缓慢滚动区域。

能够看出咱们在不经意间就已经制造出了不少意料以外的合成层,这些没有实际意义的合成层都是能够被优化的。

3、一些优化建议

一、动画使用 transform 实现

对于一些体验要求较高的关键动画,好比一些交互复杂的玩法页面,存在持续变化位置的 animation 元素,咱们最好是使用 transform 来实现而不是经过改变 left/top 的方式。这样作的缘由是,若是使用 left/top 来实现位置变化,animation 节点和 Document 将被放到了同一个 GraphicsLayer 中进行渲染,持续的动画效果将致使整个 Document 不断地执行重绘,而使用 transform 的话,可以让 animation 节点被放置到一个独立合成层中进行渲染绘制,动画发生时不会影响到其它层。而且另外一方面,动画会彻底运行在 GPU 上,相比起 CPU 处理图层后再发送给显卡进行显示绘制来讲,这样的动画每每更加流畅。

二、减小隐式合成

虽然隐式合成从根本上来讲是为了保证正确的图层重叠顺序,但具体到实际开发中,隐式合成很容易就致使一些无心义的合成层生成,归根结底其实就要求咱们在开发时约束本身的布局习惯,避免踩坑。

好比上边提到的栏目页面,就由于平时开发的不注意形成页面生成了过多的合成层,我在试图查看页面合成层状况的时候,在 PC 上已经能明显感到卡顿了。利用 Chrome Devtools 分析以后不难发现,页面里边存在的一个带动画 transform 的 button 按钮,提高为了合成层,动画交叠的不肯定性使得页面内其余 z-index 大于它但其实并无交叠的节点也都所有提高为了合成层(这个缘由真的好坑)。

这个时候咱们只须要把这个动画节点的 z-index 属性值设置得大一些,让层叠顺序高过于页面其余无关节点就行。固然并非盲目地设置 z-index 就能避免,有时候 z-index 也仍是会致使隐式合成,这个时候能够试着调整一下文档中节点的前后顺序直接让后边的节点来覆盖前边的节点,而不用 z-index 来调整重叠关系。方法不是惟一的,具体方式仍是得根据不一样的页面具体分析。

改善后的页面效果以下,能够看到相比优化前,咱们消除了不少无心义的合成层。

三、减少合成层的尺寸

举个简单的例子,分别画两个尺寸同样的 div,但实现方式有点差异:一个直接设置尺寸 100x100,另外一个设置尺寸 10x10,而后经过 scale 放大 10 倍,而且咱们让这两个 div 都提高为合成层:

<style> .bottom, .top { position: absolute; will-change: transform; } .bottom { width: 100px; height: 100px; top: 20px; left: 20px; z-index: 3; background: rosybrown; } .top { width: 10px; height: 10px; transform: scale(10); top: 200px; left: 200px; z-index: 5; background: indianred; } </style>
<body>
  <div class="bottom"></div>
  <div class="top"></div>
</body>
复制代码

利用 Chrome Devtools 查看这两个合成层的内存占用后发现,.bottom 内存占用是 39.1 KB,而 .top 是 400 B,差距十分明显。这是由于 .top 是合成层,transform 位于的 Composite 阶段,如今彻底在 GPU 上执行。所以对于一些纯色图层来讲,咱们可使用 width 和 height 属性减少合成层的物理尺寸,而后再用 transform: scale(…) 放大,这样一来能够极大地减小层合成带来的内存消耗。

参考文章

CSS3硬件加速也有坑
无线性能优化:Composite
CSS GPU Animation
详谈层合成


若是你以为这篇内容对你有价值,请点赞,并关注咱们的官网和咱们的微信公众号(WecTeam),每周都有优质文章推送:

WecTeam