JavaScript 动画
在《布局与重排》的手记中,我们讲了重排,一旦元素的几何空间发生变化,那一定会经历布局、绘图和合成这三个阶段。这三个阶段中,尤其以布局和绘图最为耗时。
最常见的情况,比如使用 JavaScript 来制作动画,动画将完全运行在 CPU 上,并且动画的每个步骤都会引起重绘。尽管渲染引擎已经做了优化,只去重绘更新的区域,但动画效果并不顺滑。如果主线程上的 JavaScript 代码阻塞,还会引起动画卡顿现象。
例子:
<style>
#div1 {
width: 100px;
height: 100px;
color: #fff;
background: red;
padding: 5px;
position: relative;
left: 0;
}
</style>
<div id="div1">JS</div>
<button>开始</button>
<p id="txt"></p>
<script>
var btn = document.getElementsByTagName("button")[0],
div1 = document.getElementById("div1"),
txt = document.getElementById("txt"),
n = 1;
btn.onclick = function() {
btn.disabled = true;
// JS 动画启动
var timer = setInterval(function() {
div1.style.left = (n++) + "px";
if (div1.style.left === "200px") {
clearInterval(timer);
}
}, 10);
// 模拟主线程出现复杂计算
setTimeout(function() {
for (var i = 0; i < 999; i++) {
txt.innerHTML += i + "<br>";
}
}, 1000);
};
</script>
打开最新版的谷歌浏览器,可以看到:
其中,绿色表示不断有图像输出到屏幕上,时间过了一半,开始执行 JavaScript 的复杂计算( 橘黄色部分 ),CPU 高负荷运行导致动画卡住;计算完成之后,又不断有图像输出到屏幕上。
我们使用谷歌浏览器,继续向内部探索,查看输出到屏幕的每一帧动画( 每一张图片 )都经历了哪几步:
CSS 动画
实际上,更好的实现方式是使用 CSS3 的 Opacity 和 Transform 属性引入动画。CSS 动画将自动创建一个独立合成层,并且运行在 GPU 上,更为重要的是,由于在创建动画的时候,已经声明了开始位置、结束位置和持续时间等属性,渲染引擎在动画开始之前就已经准备完毕所有必需的指令( 元素大小、偏移量、不透明度等数据 ),因此,不需要重新布局和绘制,只需要改变合成参数,就可以实现更为顺畅的动画效果。
例子:
<style>
#div1 {
width: 100px;
height: 100px;
color: #fff;
background: red;
will-change: transform;
padding: 5px;
transition: transform 2s linear;
}
</style>
<div id="div1">CSS</div>
<button>开始</button>
<script>
var btn = document.getElementsByTagName("button")[0],
div1 = document.getElementById("div1");
btn.onclick = function() {
btn.disabled = true;
div1.style.transform = "translateX(200px)";
};
</script>
通过浏览器可以看到:
在计算每一帧动画的时候,渲染引擎不需要重新布局和绘图,只是在随后使用了合成功能,并且合成阶段花费的时间非常少。
哪怕主线程的任务非常繁重,动画仍将顺畅运行。
例子:
<style>
#div1 {
width: 100px;
height: 100px;
color: #fff;
background: red;
will-change: transform;
padding: 5px;
transition: transform 2s linear;
}
</style>
<div id="div1">CSS</div>
<button>开始</button>
<p id="txt"></p>
<script>
var btn = document.getElementsByTagName("button")[0],
div1 = document.getElementById("div1"),
txt = document.getElementById("txt"),
n = 1;
btn.onclick = function() {
btn.disabled = true;
div1.style.transform = "translateX(200px)";
// 模拟主线程出现复杂计算
setTimeout(function() {
for (var i = 0; i < 999; i++) {
txt.innerHTML += i + "<br>";
}
}, 0);
};
</script>
优化建议
尽管 CSS 动画很完美,但也有其不足之处。我们可以从以下几方面进行优化:
1、主动提升合成层
如果直接使用 CSS 的 Opacity 和 Transform 属性创建动画,只有在动画执行的过程中,动画元素才会被强制提升为独立合成层,这会直接导致两个问题:首先,在动画的开始阶段,层数据由 CPU 传输到 GPU,并在 GPU 发生一次重绘( 打开页面时,已经在 CPU 绘制过一次 );然后,在动画的结束阶段,渲染引擎会立刻回收独立合成层的内存,合成层上的图像数据会由 GPU 返还 CPU,同样会在 CPU 上再次重绘。这就很容易造成在动画开始或结束时看到元素闪烁的现象。
因此,应该使用 will-change 属性将动画元素一直保持在单个合成层上。这样,渲染引擎会直接将动画完全抛给 GPU 去实现,动画的启动和结束也会变得平稳。
2、避免隐式合成
渲染引擎出于各种各样的原因,会将元素隐式提升为单独合成层,交由 GPU 去运行。在《分层策略》这篇手记中,我们列举了部分常见的隐式提升现象。最典型的例子,如果一个节点,有一个 Z-index 值比自己小的兄弟节点,且该兄弟节点是一个合成层,那么该节点也会被提升为合成层。类似这种隐式合成过多,浏览器崩溃的机率非常高。
因此,应该将动画元素的 Z-index 值设置高一些,避免复杂的嵌套。
3、尽可能使用 Opacity 和 Transform 模拟其他动画
CSS 动画中,只有 Opacity 和 Transform 属性不会造成重新布局和绘制,而其他动画则不是这样的。
例子:
<style>
#div1 {
width: 100px;
height: 100px;
will-change: transform;
background: red;
transition: background 0.4s;
}
#div1:hover {
background: blue;
}
</style>
<div id="div1"></div>
从截图中可以看到,输出到屏幕上的每一帧动画,虽然没有发生重新布局,但是发生了重绘。
如果我们使用 Opacity 模拟这种背景色切换的动画,就可以避免发生重绘。
例子:
<style>
#div1 {
width: 100px;
height: 100px;
background: red;
}
#div1::before {
content: "";
display: inline-block;
width: 100px;
height: 100px;
background: blue;
will-change: opacity;
opacity: 0;
transition: opacity 0.4s;
}
#div1:hover::before {
opacity: 1;
}
</style>
<div id="div1"></div>
如有错误,欢迎指正,本人不胜感激。