超脱的元素——浅谈如何提升前端动画性能

151 阅读5分钟

现在假设有个需求,要我们做三个方块,其中第三个方块要从初始位置往左滑动

先实现基础功能

我们先回顾一下简单的CSS动画是如何实现的


此时的代码为:

<html>  <head></head>  <body>    <div id="a" class="area"></div>    <div id="b" class="area"></div>    <div id="c" class="area"></div>  </body></html>

<style>  .area {    width: 50px;    height: 50px;    margin-bottom: 10px;  }  #a {    background-color: red;  }  #b {    background-color: blue;  }  #c {    background-color: green;    animation: slidein 5s infinite;  }  @keyframes slidein {    from {      margin-left: 0;    }    to {      margin-left: 100px;    }  }</style>


优化思路

我们通过改变margin的方式让第三个方块动了起来,好了工作完成了,该下班回家了。。。才怪,PM嫌咱们做的动画在老设备上太卡,要提高流畅度,我们该怎么优化咧?

在着手优化之前,我们得先想明白,为什么这种方式是低效的。我们都知道,浏览器渲染会进行重排和重绘,一旦文档结构发生变更,便会牵一发而动全身,导致元素布局重新计算,如果元素之间关联紧密且变动频繁,便容易出现卡顿。故而我们在进行CSS修改的时候,往往会刻意避免大量的结构变更,并且尽可能将多次CSS操作合并起来。

回到我们的动画上,动画元素天然就是不停在动的,浏览器也就不得不反复的进行着计算工作。那我们该怎么尽可能减少计算压力呢?很简单,让动的元素超脱出来,不干扰其他元素的布局即可


用绝对定位可以不

我们脑子里一瞬间就闪出了万恶的绝对定位:

<style>  .area {    position: absolute;    left: 10px;    width: 50px;    height: 50px;  }  #a {    top: 10px;    background-color: red;  }  #b {    top: 65px;    background-color: blue;  }  #c {    top: 120px;    background-color: green;    animation: slidein 5s infinite;  }  @keyframes slidein {    from {      left: 10px;    }    to {      left: 100px;    }  }</style>


绝对定位的元素确实脱离文档流了,但它真能让动画元素超脱出来,减轻浏览器负担么,我们可以用Chrome的Layers视图来看一下:

(控制台 => more => More tools => Layers)


右侧的立体图只看截图不太直观,但左侧的图层我们可以看到只有文档一层,元素并没有成功完成"飞升",这种情况下浏览器的计算工作依然巨大,所以单纯的绝对定位等脱离文档流的方式,并不能满足我们的需求呢。

如果能让动的元素成为单独的一层,其他不动的当做背景层,才算真正脱钩呢,那该怎么实现呢。


让GUP上吧

我们把CSS动画实现的代码再来改版一下:

@keyframes slidein {    from {      transform: translateX(0);    }    to {      transform: translateX(100px);    }  }

此时我们再来看一下Layers:


可以看到已经变成两层了呢,我们再换个角度截图:


可以看到绿色区域C已经完全脱离了原图层了呢,这是利用了CSS translate3d属性会使用GUP渲染的原理。

所以到目前为止我们终于可以得出一个有用的结论:动画元素尽可能用GUP渲染,最简单的是使用translate3d属性。


就这?

这种基础知识点确实没必要长篇大论说这么多,咱们接下来说说隐患。在我们的需求里,A和B元素都是不动的,只有最底层的C需要动起来,那么我们把需求改成让B动,C不用动了,图层的视角会怎么样咧?

<style>  .area {    position: absolute;    left: 10px;    width: 50px;    height: 50px;  }  #a {    top: 10px;    background-color: red;  }  #b {    top: 65px;    background-color: blue;    /* 只有B在动哦 */    animation: slidein 5s infinite;  }  #c {    top: 120px;    background-color: green;  }  @keyframes slidein {    from {      transform: translateX(0);    }    to {      transform: translateX(0);    }  }</style>


这貌似不对呀?只有B用GUP提升了,为啥会有三个图层,而且为啥看立体图C还在B上面?

这里涉及到一个知识点,在都不用GUP提升的时候,所有元素都在一个图层上,文档的上下排列顺序无所谓,不会出现谁在谁上面的问题。但一旦其中一个元素被GUP单独渲染,那么在它文档结构下面的、且脱离了文档流的元素,则也会被单独渲染为一个图层,文档结构越靠下图层层级便越高。

这样会带来什么问题呢?每个图层都将占用相应的内存,图层越多内存占用越多,随着而来的就是卡顿甚至崩溃。

那如何避免生成不想要的图层呢?也很简单,给动画元素加上z-index就可以了。


最佳实践

所以我们针对动画元素应该:

  1. 让该元素脱离文档流,比如position:relative;
  2. 让该元素用GUP渲染到独立图层,比如:transform: translateZ(0);
  3. 给该元素设置z-index: 1;

我们来看看让B动起来的最终代码:

<style>  .area {    width: 50px;    height: 50px;    margin-bottom: 10px;  }  #a {    background-color: red;  }  #b {    position: relative;    z-index: 1;    background-color: blue;    animation: slidein 5s infinite;  }  #c {    background-color: green;  }  @keyframes slidein {    from {      transform: translateX(0);    }    to {      transform: translateX(100px);    }  }</style>

以及对应图层视图: