手记

减少重绘

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>


如有错误,欢迎指正,本人不胜感激。

0人推荐
随时随地看视频
慕课网APP