最近看到一篇文章,介绍了一种针对模糊效果的动画。

目前实现模糊效果有两种方式:CSS filter 属性和 SVG filters,后者的支持度更高。 在这个 Demo中,使用了 CSS filter,定义了针对模糊半径的一个动画。

@keyframes b1-anim {
    0% { filter: blur(0px); }
    100% { filter: blur(16px); }
}

在高性能设备上能够流畅运行,而在手持设备中会有卡顿。

CSS filter 的问题

从下面的渲染流水线可以看出,blur 和 opacity transform 一样,不会触发浏览器的重绘。那么为什么会存在卡顿呢?

渲染流水线

渲染流水线

原来问题出在 blur 使用的算法上。无论是 CSS 还是 SVG,计算模糊都是使用 convolution filters。这种算法在图片像素增多和模糊半径增大时,开销会显著增大。

变通方法

既然实时变化模糊半径计算开销大,我们可以将整个模糊变化分成几个阶段,预先使用 filter 定义这几个阶段的模糊半径,然后使用 opacity 控制每个阶段的显示。

比如我们分成 4 个阶段,注意这里需要 HTML 冗余:

<div>
    <img id="b1"/>
    <img id="b2"/>
    <img id="b3"/>
    <img id="b4"/>
</div>

定义每个阶段固定的模糊半径避免实时变化,还可以结合使用 will-change: transform

@keyframes b2-anim {
  0% { opacity: 0; }
  33% { opacity: 1; }
  66% { opacity: 1; }
  67% { opacity: 0; }
  100% { opacity: 0; }
}

@keyframes b1-anim {
  0% { opacity: 1; }
  33% { opacity: 1; }
  34% { opacity: 0; }
  100% { opacity: 0; }
}

#b1 {
  will-change: transform
  animation: b1-anim 1s infinite alternate linear;
}

#b2 {
  will-change: transform
  -webkit-filter: blur(4px);
  animation: b2-anim 1s infinite alternate linear;
}

那么实际效果如何呢?在我自己的机器上运行并没有原文中那么大的提升。不过虽然最原始的方式也能保证 16ms,但是 GPU 的处理时间在采用这种方式之后,从每帧 7ms -> 3ms。

进一步优化

不难发现,主要需要优化的是冗余的 HTML,增加了之后会不会影响到原有页面中的样式规则呢? Web Components 中的 Shadow DOM 可以解决这个问题。

在上面的例子中,<div id="container"> 可以作为影子根节点的宿主节点。这样有效地划分了 CSS 和 JS 的边界。