更好的 box-shadow 动画

3,044 阅读4分钟
原文链接: bluest.me

引子

某日移动端有一需求:要求一 App Logo 有一层外阴影闪动效果,实现起来倒也不复杂。简单粗暴直接在 keyframes 中定义 box-shadow 动画即可交差,最终代码如下:

.box {
  margin: 100px auto;
  width: 200px;
  height: 200px;
  border-radius: 4px;
  animation: boxAnimation 1s infinite linear alternate-reverse;
}

@keyframes boxAnimation {
    form {
      box-shadow: 0 0 0px rgba(0, 0, 0, .4);
    }
    to {
      box-shadow: 0 0 50px rgba(0, 0, 0, .4);
    }
}

动画在 PC 端运行时我这写轮眼是看不出任何卡顿的,一旦在模拟器或者移动端上运行情况就不那么乐观了,出现了明显可感知的掉帧。

Why?

首先我们会归咎于移动端设备的性能落于 PC。是啊,红米 Note 4 的机能与 MacBook Pro 2016 顶配之间的性能差距非常巨大。但在同一移动设备下其他的动画却很流畅的,比如 transform,那为什么偏偏 box-shadow 就会导致性能问题呢?而且 box-shadow 也是个惯犯了,之前就爆出 box-shadow 在页面滚动时会导致性能问题,既然有前科这就好办了。经过查阅资料得知:

不同样式在消耗性能方面是不同的,有些效果(如经常被人提起的 box-shadow)从渲染角度来讲十分耗性能,原因就是与其他样式相比,它们的绘制代码执行时间过长。也就是说,如果一个耗性能严重的样式经常需要重绘,那么你就会遇到性能问题。
via Scrolling Performance

keyframes 中定义的动画是循环改变 box-shadow,导致浏览器会一直重绘耗性能严重的样式,进而产生性能问题。

解决问题嘛,可不可以先解决提出问题的 CSS 属性 —— box-shadow 呢?我觉得删除是不可能删除的,这辈子都不可能删除的,box-shadow 长得又好看,用起来还简单,我超喜欢用的!那么优化思路就剩下如何阻止浏览器一直绘制 box-shadow

How

至于怎么做 CSS-TRICKS 直接给出了解决方案:为伪元素设置 box-shadow 并对其不透明度设置动画:

How do you animate the box-shadow property in CSS without causing re-paints on every frame, and heavily impacting the performance of your page? Short answer: you don’t. Animating a change of box-shadow will hurt performance.

There’s an easy way of mimicking the same effect, however, with minimal re-paints, that should let your animations run at a solid 60 FPS: animate the opacity of a pseudo element.

trick by Tobias Ahlin:

根据其建议,我们将代码修改如下,果然模拟器或移动端设备上动画不卡了!打开 Chrome DevTools 中帧率观察器,帧率也一直是以 60fps 的一条直线,而不是之前的略有波动。

.box {
  margin: 100px auto;
  width: 200px;
  height: 200px;
  border-radius: 4px;
}

.box::after {
  content: '';
  // 防止遮蔽父层
  position: absolute;  
  z-index: -1;
  display: block;
  width: inherit;
  height: inherit;
  border-radius: inherit;
  box-shadow: 0 0 50px rgba(0,0,0,.4);
  will-change: opacity;
  opacity: 0;
  animation: fadeIn 1s infinite linear alternate-reverse;
}

@keyframes fadeIn {
    form {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
}

opacity 一边乐呵大唱“我们不一样”!那问题来了,为何 opacity 不会引发浏览器的重绘?

有一个 CSS 属性,你可能认为它会引起重绘,但有时候并不会。就是:opacity。当GPU在合成元素的纹理结构的时候,会以一个较低的 alpha 值去处理 opacity 的改变。它的条件是,元素必须是图层中唯一的一个元素。如果它和其它的元素组合在一起,那么对 opacity 的改变也会让 GPU(错误地)淡化其它的元素。
via High Performance Animations

原来 opacity 真的不一样啊!因为在 Blink 和 WebKit 内核的浏览器中,对于在 CSS transition 或者 animation 中有 opacity 改变的元素,浏览器改将会为其创建一个图层,然后交给其小秘 GPU 去分担处理。同样能享受这些待遇的动画属性如下:

现代浏览器在完成以下四种属性的动画时,消耗成本较低:position(位置)1, scale(比例缩放), rotation(旋转) 和 opacity(透明度)。
via High Performance Animations

上文可知 scale 也是不错的备选方案,但我为什么没有选?如果对伪元素做 scale 动画会导致动画效果跟 box-shadow 有差异。既然创建一个交由 GPU 渲染的图层这么厉害,那么我们可不可以强制创建一个?那就是 前端交互动画优化 中提到的认为产生硬件加速了。

最近的文章都离不开浏览器的回流与重绘,希望自己接下来能整理下相关的知识点(无意立了个 flag,假装没看见好啦)。

参考

  1. How to animate "box-shadow" with silky smooth performance
  2. High Performance Animations
  3. Scrolling Performance
  4. CSS Paint Times and Page Render Weight

  1. 这里指代是 translate 或者 translate3d