用Anime.js打造流畅动画的实战技巧与踩坑经验

Mr.建英 交互 阅读 2,586
赞 12 收藏
二维码
手机扫码查看
反馈

动画卡顿到怀疑人生,原来是 anime.js 的 timeline 用错了

上周在做一个产品页的交互动画,用 anime.js 做一个分步入场的效果。本来以为就是简单地串几个动画,结果页面一跑起来,动画卡得像幻灯片,尤其在低端安卓机上直接掉帧到怀疑人生。我一开始还以为是 CSS 动画性能问题,后来折腾了半天才发现,坑出在 anime.js 的 timeline 用法上。

用Anime.js打造流畅动画的实战技巧与踩坑经验

这里我踩了个大坑:一开始图省事,直接用 anime() 调了多个动画,靠 delay 控制时间:

anime({
  targets: '.step1',
  opacity: [0, 1],
  duration: 300,
  delay: 0
});

anime({
  targets: '.step2',
  opacity: [0, 1],
  translateY: [-20, 0],
  duration: 300,
  delay: 300
});

anime({
  targets: '.step3',
  opacity: [0, 1],
  translateY: [-20, 0],
  duration: 300,
  delay: 600
});

看起来没问题对吧?但实际跑起来,尤其是快速切换页面再回来,或者连续触发多次,动画就乱套了。有时候 step2 没动,有时候全部一起闪出来。更诡异的是,有些设备上明明 delay 设置了,却像是同时执行。

我一开始以为是浏览器渲染问题,还去查了 will-change、transform3d 这些优化手段,加了一堆 CSS 也没用。后来才意识到:**每次调用 anime() 都会创建一个独立的动画实例,它们之间没有真正的时序同步机制**。虽然 delay 看似能控制时间,但一旦页面有重排、JS 执行阻塞,或者用户快速操作,这些独立的动画就会“脱节”。

折腾了半天,翻了 anime.js 的文档,才看到它有个叫 timeline 的东西。这玩意儿才是做串行动画的正道。于是我把代码改成这样:

const tl = anime.timeline({
  easing: 'easeOutQuad',
  duration: 300
});

tl
  .add({
    targets: '.step1',
    opacity: [0, 1]
  })
  .add({
    targets: '.step2',
    opacity: [0, 1],
    translateY: [-20, 0]
  })
  .add({
    targets: '.step3',
    opacity: [0, 1],
    translateY: [-20, 0]
  });

这一改,流畅度立马回来了。timeline 内部维护了一个统一的时间轴,所有 .add() 的动画都按顺序排队执行,不会因为外部干扰而错乱。而且,它还能自动处理动画的开始和结束时间,不需要手动算 delay。

但别急着高兴,这里还有个隐藏坑:**如果用户快速点击触发动画多次,旧的 timeline 还没结束,新的又开始了,照样会乱**。我一开始没处理这个,测试的时候发现连点两下,元素就抖成帕金森了。

解决办法也很简单:在触发动画前,先停掉可能存在的旧动画。anime.js 提供了 pause()restart(),但更直接的是用 seek(0) + play(),或者干脆在创建新 timeline 前清掉旧的引用。不过最稳妥的做法是——**给 timeline 加个状态锁**。

我最后的完整方案是这样的:

let currentAnimation = null;

function playIntro() {
  // 如果有正在运行的动画,先干掉
  if (currentAnimation) {
    currentAnimation.pause();
    // 可选:重置元素状态
    anime.set('.step1, .step2, .step3', {
      opacity: 0,
      translateY: -20
    });
  }

  currentAnimation = anime.timeline({
    easing: 'easeOutQuad',
    duration: 300,
    complete: () => {
      currentAnimation = null; // 动画结束,释放引用
    }
  });

  currentAnimation
    .add({
      targets: '.step1',
      opacity: [0, 1]
    })
    .add({
      targets: '.step2',
      opacity: [0, 1],
      translateY: [-20, 0]
    })
    .add({
      targets: '.step3',
      opacity: [0, 1],
      translateY: [-20, 0]
    });
}

这样,无论用户点多少次,旧动画都会被清理,新动画从头开始。亲测有效,连点十次都不抖了。

另外,我还试过用 anime()loopdirection,但发现对于这种一次性、分步骤的入场动画,timeline 还是最清晰的。而且 timeline 支持嵌套,比如某个步骤里还要再分两个子动画,也能用 .add() 里再套一个 timeline,灵活性很高。

不过有个小问题到现在还没完美解决:在 Safari 上,如果页面在后台(比如切到其他 tab),再切回来,timeline 有时会“跳帧”——直接跳到当前应该在的位置,而不是平滑过渡。这其实是浏览器为了省电暂停了 requestAnimationFrame 导致的,不光 anime.js 有这问题,所有基于 RAF 的动画库都有。目前我的 workaround 是监听 visibilitychange 事件,页面隐藏时 pause,显示时 restart,但体验还是有点突兀。好在不影响主流程,暂时先放着。

再唠叨一句:**别用 delay 模拟串行**!看似简单,实则隐患多。timeline 才是正解。而且 timeline 的 API 很直观,.add() 就是加步骤,支持 offset(比如 ‘-=100’ 实现重叠),比手算 delay 精确多了。

如果你的动画涉及多个元素按顺序动,或者需要精确控制节奏,直接上 timeline。别像我一样,一开始图快,结果花两倍时间 debug。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有更优雅的方式处理重复触发动画?或者 Safari 后台恢复的平滑方案?求指教。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论