用Anime.js打造流畅动画的实战技巧与踩坑经验
动画卡顿到怀疑人生,原来是 anime.js 的 timeline 用错了
上周在做一个产品页的交互动画,用 anime.js 做一个分步入场的效果。本来以为就是简单地串几个动画,结果页面一跑起来,动画卡得像幻灯片,尤其在低端安卓机上直接掉帧到怀疑人生。我一开始还以为是 CSS 动画性能问题,后来折腾了半天才发现,坑出在 anime.js 的 timeline 用法上。
这里我踩了个大坑:一开始图省事,直接用 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() 的 loop 或 direction,但发现对于这种一次性、分步骤的入场动画,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 后台恢复的平滑方案?求指教。

暂无评论