CSS 动画:基础知识与性能优化

23 篇文章 1 订阅

1 CSS 动画知识

贝塞尔曲线

贝塞尔曲线由控制点定义(2个,3个,甚至更多)。其特点包括:

  1. 控制点不总在曲线上。
  2. 曲线的阶次 = 控制点的数量 - 1。
  3. 曲线总在控制点的凸包内部(在二维欧几里得空间中,凸包可想象为一条刚好包着所有点的橡皮圈)。

绘制贝塞尔曲线方法:

  1. 控制点通过线段连接得到 n - 1 条线段:1 → 2、2 → 3 和 3 → 4,
  2. 对于 [0, 1] 的每个 t,在第一步的线段分别距起点距离比例为 t 的位置取点,连接比例点,得到 n - 2 条线段
  3. 第二步得到的线段上同样按比例 t 取点,得到 n - 3 条线段
  4. 不断重复,直到只得到一个点,该点即位于曲线上。

动画最小时间间隔

多数显示器默认频率是 60Hz,即 1 秒刷新 60 次,所以理论上最小间隔为 1 / 60 * 1000ms = 16.7ms。

trainsition 过渡(CSS3)

只需要为某个属性定义如何动态地表现其不同状态切换(比如伪元素 :hover,:active 和 JavaScript 实现的状态变化)的变化(过渡动画由浏览器生成)。可以为一个或多个 CSS 属性指定过渡效果,多个属性的过渡用逗号分隔:transition: font-size 3s, color 2s。

过渡 transition 属性是以下四个属性的简写属性:

  1. transition-property:none 表示无动画,all 表示应用在所有可动画的属性上(检查所有属性,耗费性能,不推荐)。
  2. transition-duration:动画持续的时间(单位:s或ms),默认为 0。
  3. transition-timing-function:
    1. 默认值是 ease,可设置为贝塞尔曲线(Bezier curve)或者阶跃函数(steps),
    2. 贝塞尔曲线横轴为时间(0 —— 开始时刻,1 —— transition-duration的结束时刻),纵轴为过渡动画的完成度(0 —— 属性的起始值,1 —— 属性的最终值。贝塞尔曲线表示的时间函数有四个控制点,固定的第一个点(0,0)和最后一个点(1,1),设置剩下的第二个点(x2,y2),第三个点(x3,y3),对应时间函数 cubic-bezier(x2, y2, x3, y3)。y2,y3 可以为负数或很大,即反向运动(即曲线相当于位移,一阶导数相当于速度,二阶导数相当于加速度)。内置的贝塞尔曲线有:linear(线性)、ease(慢快慢)、ease-in(由慢到快)、ease-out(由快到慢) 和 ease-in-out(很慢-慢-快-慢-很慢)
    3. 阶跃函数 steps(n, <jumpterm>) 定义了将输出值域分为等距步长的阶跃函数,也称为阶梯函数,比如时钟转动,按照 n 个定格在过渡中显示动画迭代,每个定格等长时间显示,每个停留点的持续时间为总动画时间的 1/n。具体如何跳跃取决于跳跃项 <jumpterm>。例如,如果 n 为 5,则有 5 个步骤:
      • jump-start 跳过初始位置,即在 20%、40%、60%、80% 和 100% 处暂停。
      • jump-end 是默认值,跳过结束位置,因此最后一个跳跃发生在动画结束时,即在0%、20%、40%、60% 和 80% 处暂停。
      • jump-none 两端都没有跳过,即在包括 0% 和 100% 的情况下设置 5 个定格(在 0%、25%、50%、75% 和 100% 处)。
      • jump-both 两端都跳过,即在 0% 和 100% 之间设置 5 个定格(在 100 / 6 %、100 / 6 * 2%、100 / 6 * 3%、100 / 6 * 4% 和 100 / 6 * 5% 处)。
      • start 等同于 jump-start。
      • end 等同于 jump-end。
      • step-start 等同于 steps(1, jump-start)。
      • step-end 等同于 steps(1, jump-end)。
    4. transition-delay:动画开始前的延迟时间(单位:s 或 ms)(若为负数,动画会被截断,从动画的“-delay”的阶段立刻开始)。

动画完成触发 transitionend 事件:具有 event 对象

        1. event.propertyName:当前完成动画的属性

        2. event.elapsedTime:动画已完成的时间(按秒计算),不包括 transition-delay。

每个 transition 完成都会触发一次 transitionend 事件,如果需要防止多次触发,可以采用短间隔防抖或 addEventListener 设置 options.once 为 true。如果在某个 transition 完成前,该 transition 已从目标节点上移除,那么 transitionend 将不会被触发。一种可能的情况是修改了目标节点的 transition-property 属性,另一种可能的情况是 display 属性被设为 "none"。

transition 注意事项?

1. 将隐藏元素(display: none)变成显示或插入元素(appendChild 方法)可能不能和 transition 一起使用。因为元素将视为没有开始状态,始终处于结束状态,后续对样式改变则不会有过渡动画,解决办法是异步改变样式,即在延迟几毫秒的 setTimeout() 中改变样式。

2. 解决闪动问题,使用 3D 属性 perspective、backface-visibility、translate3d(0, 0, 0)。

3. 缺点适合简单动画,JavaScript 动画更灵活实现任何动画逻辑。

animation 动画(CSS3)

一个关键帧动画由 animation 属性设置,可以指定一组或多组动画,每组之间用逗号相隔,animation 属性是以下属性的简写:

1. animation-name 属性指定关键帧动画名称由 @keyframes 指定。需同时配置 animation-duration 属性,否则时长为 0,动画不会播放。

2. animation-duration 属性规定元素动画播放完成一个周期所持续时间,以秒或毫秒计量。默认为 0 表示没有动画效果。

3. animation-delay 属性规定执行动画前的等待时间,默认为 0。负值,表示跳过对应时长进入动画 。

4. animation-timing-function 属性指定动画播放的速度曲线,同 transition-timing-function,其中 animation-timing-function 作用于一个关键帧周期而非整个动画周期,即从关键帧开始到关键帧结束,包括 steps(n, [<jumpterm>]) 阶跃函数也是如此。

5. animation-iteration-count 属性规定动画的播放次数,可选具体次数或者无限(infinite),默认为 1。可以用小数定义循环,来播放动画周期的一部分:例如,0.5 将播放到动画周期的一半。不可为负值。

6. animation-direction 属性规定动画是否在下个周期逆向播放。

7. animation-play-state 属性规定动画的播放状态,用此来控制动画的暂停和继续。running 指定运行的动画(默认值);pause 指定暂停的动画;

8. animation-fill-mode 属性规定在执行之前和之后如何将样式应用于其目标。

动画事件

animationstart 事件会在 CSS 动画开始时触发。如果有 animation-delay 延时,事件会在延迟时效过后立即触发。为负数的延时时长会致使事件被触发时事件的 elapsedTime 属性值(动画运行时长)等于该时长的绝对值(并且,相应地,动画将直接播放该时长绝对值之后的动画)。

animationend 当 CSS 动画完成时事件被触发。如果动画在完成之前终止,例如元素从 DOM 中删除或动画从元素中删除,则不会触发 animationend 事件。

animationiteration 当 CSS 动画的一个迭代周期结束,另一个迭代周期开始时,animationiteration 事件被触发。此事件不会与 animationend 事件同时发生,因此对于动画迭代 animation-iteration-count 计数为 1 的动画不会发生。

CSS animation 支持性检测:

function cssAnimationDetect() {
  let [animation, animationString, keyframePrefix] = [false, 'animation', ''];
  const elem = document.createElement('div');
  const domPrefixes = ['Webkit', 'Moz', 'O', 'ms', 'Khtml'];

  if (elem.style.animationName !== undefined) {
    animation = true;
  } else {
    for (domPrefix of domPrefixes) {
      if (elem.style[`${domPrefix}AnimationName`] !== undefined) {
        animationString = `${domPrefix}Animation`
        keyframePrefix = `-${domPrefix.toLowerCase()}-`;
        animtion = true;
        break;
      }
    }
  }
  return {
    animation,
    animationString,
    keyframePrefix
  }
}

2 流畅动画的标准

理论上说,FPS 越高,动画会越流畅,目前大多数设备的屏幕刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果最好,也就是每帧的消耗时间(帧预算)为 16.67ms,但实际上,浏览器有整理工作要做,因此所有工作需要尽量在 10 ms 内完成。而且新的智能手机有 90Hz 的屏幕而 iPad Pro 的屏幕是 120Hz 的,这会让帧预算分别减少到 11.1ms 和 8.3ms。

  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;
  • 帧率波动很大的动画,亦会使人感觉到卡顿。

CSS 动画的性能一般要优于 Javascript 动画:GPU 硬件加速 CSS 动画 > 非硬件加速 CSS 动画 > Javascript 动画。

对于动画而言,衡量一个动画的标准也就是 FPS 值。关键是如何计算出每个动画运行时的帧率,使用的是 requestAnimationFrame,原理如下

正常而言 requestAnimationFrame 这个方法在一秒内会执行 60 次,也就是不掉帧的情况下。假设动画在时间 A 开始执行,在时间 B 结束,耗时 x ms。而中间 requestAnimationFrame 一共执行了 n 次,则此段动画的帧率大致为:n / (B - A)。核心代码如下,能近似计算每秒页面帧率,以及我们额外记录一个 allFrameCount,用于记录 rAF 的执行次数,用于计算每次动画的帧率 :

const rAF = function () {
    return (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 1000 / 60);
        }
    );
}();

let frame = 0;
let allFrameCount = 0;
let lastTime = Date.now();
let lastFameTime = Date.now();

const loop = function () {
    const now = Date.now();
    let fs = (now - lastFameTime);
    let fps = Math.round(1000 / fs);

    lastFameTime = now;
    // 不置 0,在动画的开头及结尾记录此值的差值算出 FPS
    allFrameCount++;
    frame++;

    if (now > 1000 + lastTime) {
        fps = Math.round((frame * 1000) / (now - lastTime));
        // console.log('每秒 FPS:', fps);
        frame = 0;
        lastTime = now;
    };

    rAF(loop);
}

3 深入优化 CSS 动画

完整的像素管道流程 —— JS / CSS > 样式计算 > 布局 > 绘制 > 合成:

JavaScript  / CSS 。一般来说,会使用 JavaScript 或者 CSS Animations、Transitions 和 Web Animation API 来实现一些视觉变化的效果。

  1. 样式计算。此过程是根据匹配选择器计算出哪些元素应用哪些 CSS 3. 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。

  2. 布局。在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。

  3. 绘制。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。

  4. 合成。由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

当然,不一定每帧都总是会经过管道每个部分的处理。我们的目标就是,每一帧的动画,对于上述的管道流程,能避免则避免,不能避免则最大限度优化

1. 精简 DOM ,合理布局

精简的 DOM 结构在任何时候都是对页面有帮助的。

2. 使用 transform 代替 left、top,减少使用耗性能样式;

不同样式在消耗性能方面是不同的,现代浏览器在完成以下四种属性的动画时,会开启了 GPU 硬件加速,动画元素生成了独立的图形层(GraphicsLayer),消耗成本较低:

  • position(位置): transform: translate(n px, n px)
  • scale(比例缩放):transform: scale(n)
  • rotation(旋转) :transform: rotate(n deg)
  • opacity(透明度):opacity: 0...1

如果可以,尽量只使用上述四种属性去控制动画,但是,在现在性能很差的样式,可能明天就被优化,而且不同浏览器之家可能存在差异。

开启 GPU 加速的方法我们可以使用:

{
  will-change: transform
}

这会使声明了该样式属性的元素生成一个图形层,告诉浏览器接下来该元素将会进行 transform 变换,让浏览器提前做好准备。

使用 will-change 虽然并不一定会有性能的提升,因为浏览器也会运行布局和绘制流程,但是提前告诉浏览器,如此一开始就创建新的图层比等到需要时匆忙地创建要好。

值得注意的是,用好这个属性并不是很容易:

  • 在一些低端设备上,will-change 会导致很多小问题,譬如会使图片模糊,有的时候很容易适得其反,所以使用的时候还需要多加测试。

  • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。

  • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。

  • 不要过早应用 will-change 优化:如果页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致生成大量图层,进而导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程,这会导致更严重的性能问题。

  • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

3. 控制频繁动画的层级关系;

动画层级的控制的意思是尽量让需要进行 CSS 动画的元素的 z-index 保持在页面最上方,避免浏览器创建不必要的图形层(GraphicsLayer),能够很好的提升渲染性能。

简单来说,浏览器为了提升动画的性能,为了在动画的每一帧的过程中不必每次都重新绘制整个页面。在特定方式下可以触发生成一个合成层,合成层拥有单独的 GraphicsLayer。需要进行动画的元素包含在这个合成层中,这样动画的每一帧只需要去重新绘制这个 Graphics Layer 即可,从而达到提升动画性能的目的。从目前来说,满足以下任意情况便会创建层:

  • 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)
  • 硬件加速的插件,比如 flash 等等
  • 使用加速视频解码的 <video> 元素
  • 3D 或者 硬件加速的 2D Canvas 元素
  • 3D 或透视变换 (perspective、transform) 的 CSS 属性
  • 对自己的 opacity 做 CSS 动画或使用一个动画变换的元素
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素

假设我们有一个轮播图,有一个 ul 列表,结构如下:

<div class="container">
    <div class="swiper">轮播图</div>
    <ul class="list">
        <li>列表li</li>
        <li>列表li</li>
        <li>列表li</li>
        <li>列表li</li>
    </ul>
</div>

.swiper {
    position: static;
    animation: 10s move infinite;
}
    
.list {
    position: relative;
}

@keyframes move {
    100% {
        transform: translate3d(10px, 0, 0);
    }
}

如上图所示,由于给 .swiper 添加了 translate3d(10px, 0, 0) 动画,所以它会生成一个 Graphics Layer,用开发者工具可以打开层的展示,图形外的黄色边框即代表生成了一个独立的复合层,拥有独立的 Graphics Layer 。但是,并没有给下面的 list 也添加任何能触发生成 Graphics Layer 的属性,但是它也同样也有黄色的边框,生成了一个独立的复合层。

原因在于 元素有一个 z-index 较低且包含一个复合层的兄弟元素(.swiper)

使用 Chrome,我们也可以观察到这种层级关系,可以看到 .list 的层级高于 .swiper

所以,需要将 CSS 修改,明确使得 .swiper 的层级高于 .list ,再打开开发者工具观察发现,.list 元素已经没有了黄色外边框,说明此时没有生成 Graphics Layer ,也就是说,需要硬件加速的.swiper 保持在最上方, 那么每次动画过程中只会独立重绘这部分的区域。:

综上所述:

  • GPU 硬件加速也会有坑,当我们希望使用利用类似 transform: translate3d() 这样的方式开启 GPU 硬件加速,一定要注意元素层级的关系,尽量保持让需要进行 CSS 动画的元素的 z-index 保持在页面最上方。

  • Graphics Layer 不是越多越好,每一帧的渲染内核都会去遍历计算当前所有的 Graphics Layer ,并计算他们下一帧的重绘区域,所以过量的 Graphics Layer 计算也会给渲染造成性能影响。

  • 可以使用 Chrome ,用上面介绍的两个工具对自己的页面生成的 Graphics Layer 和元素层级进行观察然后进行相应修改。

4. 使用 dev-tool 时间线 timeline 观察,找出导致高耗时、掉帧的关键操作。

        1)对比屏幕快照,观察每一帧包含的内容及具体的操作

        2)找到掉帧的那一帧,分析该帧内不同步骤的耗时占比,进行有针对性的优化

        3)观察是否存在内存泄漏

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值