博客 有更多精品文章哟。
Composite 的优化
终于,我们到了像素管道的末尾。对于这一部分的优化策略,我们可以从为什么需要 Composited Layer(Graphics Layer)来入手。这个问题我们在构建 Graphics Layer Tree 的时候,已经说明过,现在简单回顾一下:
- 避免不必要的重绘。
- 利用硬件加速高效实现某些 UI 特性。
根据 Composited Layer 的这两个特点,可以总结出以下几点优化措施。
使用 transform 和 opacity 属性来实现动画
上文我们说过像素管道的 Layout 和 Paint 部分是可以略过,只进行 Composite 的。实现这种渲染方式的方法很简单,就是使用只会触发 Composite 的 CSS 属性;目前,满足这个条件的 CSS 属性,只有 transform 和 opacity。
使用 transform 和 opacity 需要注意的是:元素必须是 Composited Layer;如果不是,Paint 还是会照常触发(Layout 要看情况,一般 transform 会触发)。来看一个例子:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><style>.div {width: 100px;height: 100px;background-color: #f00;/* will-change: transform; */}</style><title>性能优化</title>
</head><body><div class="div"></div><script>const div = document.querySelector(".div");const run = () => {div.style.transform = "translate(0, 100px)";};setTimeout(run, 2000);</script>
</body>
</html>
我们将使用 transform 来向下位移,开始我们先不把 div 节点提升为 Composited Layer;通过下图可以看到:还是会触发 Layout 和 Paint 的。
这时,把 div 节点提升为 Composited Layer,我们发现 Layout 和 Paint 已经被略过了,符合我们的预期。
减少绘制的区域
如果不能避免绘制,我们就应该尽可能减少需要重绘的区域。例如,页面顶部有一块固定区域,当页面某个其他区域需要重绘的时候,很可能整块屏幕都要重绘,这时,固定区域也会被波及到。像这种情况,我们就可以把需要重绘或者受到影响的区域提升为 Composited Layer,避免不必要的绘制。
提升成 Composited Layer 的最佳方式是使用 CSS 的 will-change 属性,它的详细说明可以查看 MDN 的文档。
.element {will-change: transform;
}
对于不支持的浏览器,最简单的 hack 方法,莫过于使用 3D 变形来提升为 Composited Layer 了。
.element {transform: translateZ(0);
}
根据上文所讲的例子,我们尝试使用 will-change 属性来让固定区域避免重绘。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><style>.div {width: 100px;height: 100px;background-color: #f00;}.header {position: fixed;z-index: 9999;width: 100%;height: 50px;background-color: #ff0;/* will-change: transform; */}</style><title>性能优化</title>
</head><body><header class="header">固定区域</header><div class="div">变动区域</div><script>const div = document.querySelector(".div");const run = () => {div.style.opacity = 0.5;};setTimeout(run, 2000);</script>
</body>
</html>
首先,我们来看下没有经过优化的情况;顺带说明查看浏览器一帧绘制详情的过程。
- 打开控制台的 Performance 界面。
- 点击设置(标记 1),开启绘制分析仪(标记 2)。
- 启动 Record(标记 3),获取到想要的信息后,点击 Stop(标记 4), 停止 Record。
- 点击这一帧的 Paint(标记 5)查看绘制详情。
- 切换到 Paint Profiler 选项卡(标记 6),查看绘制的步骤。
通过上面的图片(标记 7 和标记 8)可以看到,固定区域的确被波及到,并且触发重绘了。我们再对比使用 will-change 属性优化过的情况,发现固定区域没有触发重绘。
并且,我们也可以通过一帧(标记 1)的布局详情(标记 2),查看固定区域(标记 3)是不是提升成 Composited Layer(标记 4),才避免的不必要绘制。
合理管理 Composited Layer
提升成 Composited Layer 的确会优化性能;但是,要知道创建一个新的 Composited Layer 必须要额外的内存和管理,这是非常昂贵的代价。所以,在内存资源有限的设备上,Composited Layer 带来的性能提升,很可能远远抵不上创建多个 Composited Layer 的代价。同时,由于每一个 Composited Layer 的位图都需要上传到 GPU;所以,不免需要考虑 CPU 和 GPU 之间的带宽以及用多大内存处理 GPU 纹理的问题。
我们通过 1000 个 div 节点,来对比普通图层与提升成 Composited Layer 之后的内存使用情况。可以发现差距还是比较明显的。
最小化提升
通过上文的说明,我们知道 Composited Layer 并不是越多越好。尤其是,千万不要通过下面的代码提升页面的所有元素,这样的资源消耗将是异常恐怖的。
* {/* or transform: translateZ(0) */will-change: transform;
}
最小化提升,就是要尽量降低页面 Composited Layer 的数量。为了做到这一点,我们可以不把像 will-change 这样能够提升节点为 Composited Layer 的属性写在默认状态中。至于这样做的原因,我会在下面讲解。
看这个例子,我们先把 will-change 属性写在默认状态里;然后,再对比去掉这个属性后渲染的情况。
.box {width: 100ox;height: 100px;background-color: #f00;will-change: transform;transition: transform 0.3s;
}
.box:hover {transform: scale(1.5);
}
使用 will-change 属性提升的 Composited Layer:
普通图层:
我们发现区别仅在于,动画的开始和结束,会触发重绘;而动画运行的时候,删除或使用 will-change 是没有任何分别的。
我们在构建 Graphics Layer Tree 的时候讲到过这样一条理由:
对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升的 Composited Layer 会恢复成普通图层)。
这条理由赐予了我们动态提升 Composited Layer 的权利;因此我们应该多利用这一点,来减少不必要的 Composited Layer 的数量。
防止层爆炸
我们在 Graphics Layer Tree 中介绍过层爆炸,它指的是由于重叠而导致的大量额外 Composited Layer 的问题。浏览器的层压缩可以在很大程度上解决这个问题,但是,有很多特殊的情况,会导致 Composited Layer 无法被压缩;这就很可能产生一些不在我们预期中的 Composited Layer,也就是说还是会出现大量额外的 Composited Layer。
在层压缩这一节,我们已经给出了使用层压缩优化的例子,这里就不再重复了。下面再通过解决一个无法被层压缩的例子,来更为深入的了解如何防止层爆炸。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><style>.animating {width: 300px;height: 30px;line-height: 30px;background-color: #ff0;will-change: transform;transition: transform 3s;}.animating:hover {transform: translateX(100px);}ul {padding: 0;border: 1px solid #000;}.box {position: relative;display: block;width: auto;background-color: #00f;color: #fff;margin: 5px;overflow: hidden;}.inner {position: relative;margin: 5px;}</style><title>性能优化</title>
</head><body><div class="animating">动画</div><ul><li class="box"><p class="inner">提升成合成层</p></li><li class="box"><p class="inner">提升成合成层</p></li><li class="box"><p class="inner">提升成合成层</p></li><li class="box"><p class="inner">提升成合成层</p></li><li class="box"><p class="inner">提升成合成层</p></li></ul>
</body>
</html>
当我们的鼠标移入 .animating 元素的时候,通过查看 Layers 面板,可以很清晰的看到出现的大量 Composited Layer。
这个例子虽然表面上看起来没有发生重叠;但是,因为在运行动画的时候,很可能与其他元素造成重叠,所以 .animating 元素会假设兄弟元素在一个 Composited Layer 之上。这时,又因为 .box 元素设置了 overflow: hidden; 导致自己与 .animating 元素有了不同的裁剪容器(Clipping Container),所以就出现了层爆炸的现象。
解决这个问题的办法也很简单,就是让 .animating 元素的 z-index 比其他兄弟元素高。因为 Composited Layer 在普通元素之上,所以也就没有必要提升普通元素,修正渲染顺序了。这里我在顺便多说一句,默认情况下 Composited Layer 渲染顺序的优先级是比普通元素高的;但是在普通元素设置 position: relative; 之后,因为层叠上下文,并且在文档流后面的原因,所以会比 Composited Layer 的优先级高。
.animating {position: relative;z-index: 1;...
}
当然,如果兄弟元素一定要覆盖在 Composited Layer 之上,那我们也可以把 overflow: hidden; 或者 position: relative; 去掉,来优化 Composited Layer 创建的数量或者直接就不创建 Composited Layer。
参考资料
- 无线性能优化:Composite
- 坚持仅合成器的属性和管理层计数
- 简化绘制的复杂度、减小绘制区域
- CSS Animation性能优化
- 使用CSS3 will-change提高页面滚动、动画等渲染性能
- CSS3硬件加速也有坑
- 深入理解CSS中的层叠上下文和层叠顺序
总结
本文首先讲了渲染需要构建的一些树,然后通过这些树与像管道各部分的紧密联系,整理了一些优化措施。例如,我们对合成所进行的优化措施,就是通过 Graphics Layer Tree 来入手的。
优化也不能盲目去做,例如,提升普通图层为 Composite Layer 来说,使用不当,反而会造成非常严重的内存消耗。应当善加利用 Google 浏览器的调试控制台,帮助我们更加详尽的了解网页各方面的情况;从而有针对性的优化网页。
文章参考了很多资料,这些资料都在每一节的末尾给出。它们具有非常大的价值,有一些细节,本文可能并没有整理,可以通过查看它们来更为深入的了解。