Lottie 动画在 Web 端的优化:从 AE 导出到运行时渲染的完整链路

本文从 AE 源文件减量、Bodymovin 导出配置、SVG/Canvas 渲染器选型到运行时生命周期管理,系统梳理 Lottie 动画在 Web 端的性能优化链路,帮助前端团队定位掉帧和内存泄漏问题,并提供可落地的工程实践建议。

Lottie 很好,但问题出在哪

Lottie 几乎是现在做前端动效的一个默认选项。设计师在 After Effects 里排好关键帧,用 Bodymovin 导出一个 JSON 文件,前端拿到后在页面上渲染成矢量动画。相比 GIF 或者视频,Lottie 看起来体积小、清晰度高,也能响应交互。可实际上,只要项目里的动画一多,或者某一帧比较复杂,Web 端就会开始出现掉帧、内存暴涨、首屏渲染被拖慢的情况。

AI technology illustration

很多团队会遇到一个问题:设计稿里的动画效果很漂亮,导出的 JSON 也只有几十 KB,但扔到页面上就是卡。这时候大家往往先去怀疑渲染器,怀疑代码写法,却很少有人注意到,真正的瓶颈从 AE 的工程文件里就已经开始了。Lottie 的优化链路不是从代码里开始的,而是从设计师的图层结构里开始的。

这篇文章不打算讲 Lottie 的基础用法,我想把从 AE 导出到运行时渲染这条完整链路梳理一遍,聊一聊每一阶段里真正影响性能的因素,以及不同规模的项目该怎么做取舍。

先说清楚 Lottie 在 Web 端的渲染链路

Lottie 在 Web 端的工作流程大概是这样:设计师在 AE 里做动画,借助 Bodymovin 插件把关键帧、形状路径、表达式、图层变换这些信息转换成一份 JSON 描述文档。前端拿到 JSON 后,由 lottie-web(或者 lottie-player 这类封装)把它解析成内部的数据结构,然后选择一种渲染器生成画面。

目前的渲染器主要有三种:SVG、Canvas、HTML。SVG 渲染器会把每一帧动画都创建成对应的 DOM 节点,缩放和交互很好做,但节点数量一多,DOM 开销就上去了;Canvas 渲染器是把每一帧画在画布上,适合图形复杂的场景,但代价是失去部分 DOM 交互能力,还要自己处理高清屏的缩放。HTML 渲染器现在用得很少,通常只适用于一些简单的 CSS 属性动画。

这里的一个关键认知是:Lottie 的性能瓶颈,往往不是 JSON 文件大小,而是每一帧需要绘制的路径点数量、图层数量,以及渲染器对图形的拆分方式。文件小只说明压缩得好,不代表渲染起来轻松。

举个例子,一个用位图图层做的动画,可能只有几百帧,但每一帧都是一张位图,在 Web 端会产生大量内存占用;而一个用纯形状图层画的写实人物动画,路径点可能有几万个,SVG 渲染时就会明显卡顿。这两种问题完全不在一个维度上。

从 AE 源文件开始:先学会给动画减重

很多人拿到设计稿后的第一反应是直接调导出参数,但如果 AE 工程本身就带着大量不健康的图层,后面再怎么优化也是有限的。我开始接 Lottie 之后的一个习惯,是先打开工程看时间轴上的图层结构。一个需要做复杂动效的 Lottie 动画,图层数量通常应该控制在 30 个以内,特殊情况下可以多一点。如果超过了五十个甚至上百个,就要考虑是不是设计上做了太多碎片化的小元素。

有几个常见的问题在 Web 端会被放大。

  • 大量位图图层:Bodymovin 虽然支持位图,但每张图都会在运行时占用内存,而且缩放时容易发虚。能用形状画的尽量用形状。
  • 复杂路径和大量锚点:一个圆角矩形生成的路径可能只需要很少的锚点,但如果设计师用了手绘笔刷或者贝塞尔曲线拖拽,路径上的锚点可能非常密集,渲染时节点数量会直接翻倍。
  • 过长的合成和过多关键帧:关键帧数量对性能影响相对小,但重复的、无变化的关键帧会冗余在 JSON 里,白白增加解析时间。
  • 各种效果器、表达式、粒子:部分 AE 效果 Bodymovin 根本不会导出,有些会导成另一种等价实现,但代价可能是性能爆炸。例如粒子系统。

所以我把这项工作称为 UI 动效的降维:把设计师习惯的像素级思维,转换成可描述的矢量图形。这里有个不一定受欢迎的结论:不是所有 AE 动画都适合做成 Lottie。如果某个效果真的需要粒子系统,或者大量位图合成,那直接用视频或者序列帧可能是更好的选择。

一个相对清晰的取舍标准可以这样判断:如果这个动画是 UI 交互反馈,持续事件短,元素不超过 20 个,并且是纯矢量图形,那 Lottie 非常适合;如果是一个需要全屏播放的品牌宣传动画,背景还带粒子,我建议你和设计师再聊一聊。

Bodymovin 导出:不是点一下导出就结束了

Bodymovin 插件的导出面板里有不少设置项,但实际项目里大部分人都只用了默认配置。其实这里藏着不少优化空间。

比较关键的几个设置:

  • 压缩 JSON:Bodymovin 可以输出压缩过的 JSON,去掉多余的空格和换行。虽然对运行影响不大,但能减少网络传输体积。
  • 禁用未使用的属性:如果某个图层完全没有使用表达式,可以关闭 expression 的导出。实际上 Bodymovin 会对表达式做二次解析,如果不需要,最好在导出前让设计师清理掉。
  • 限制帧率:Bodymovin 默认会保留 AE 工程的帧率。有些设计稿用到了 60fps,但动画本身就是缓动效果,24fps 甚至 30fps 就能达到同样视觉体验。降低帧率对运行时性能有直接帮助。
  • 去掉多余的 marker:如果你不需要用 marker 来触发事件,可以关掉。

需要额外注意的是,Bodymovin 导出的 JSON 文件里,每一个 shape 图层都会带有一组 paths 和 transforms,如果设计师用遮罩或者修剪路径做效果,实际上会生成大量额外的节点。所以导出前最好检查一下是否存在隐藏的高复杂度层。

这里给一个简单的示例,展示怎么在导出前检查 JSON 文件里的节点数量,至少心里有个数:

const fs = require('fs');
const data = JSON.parse(fs.readFileSync('animation.json', 'utf8'));
const layers = data.layers || [];
let shapeCount = 0;
layers.forEach(layer => {
  if (layer.shapes) shapeCount += layer.shapes.length;
});
console.log('图层数:', layers.length, '形状数:', shapeCount);

如果形状数远超图层数,说明每个图层内部还有很多独立子路径,性能大概率会出问题。这时候应该回到 AE 里把图形合并,或者把多个形状合并到一个图层里用组管理。

运行时渲染:选对渲染器,比什么都重要

前面说的都是动画本身的复杂度,而当动画已经拿到手上以后,剩下的优化就都发生在浏览器里。这里第一个决策就是选 SVG 还是 Canvas。

SVG 的好处是清晰、可交互、样式可控,复杂一些的交互动画(例如 hover 变色、局部点击)都能轻松实现;Canvas 则胜在绘制大量锚点和复杂图形时开销低,因为它在单个画布上绘制,不会产生大量 DOM 节点。下面是两种渲染器在 Web 端实际使用中的差异:

对比维度 SVG 渲染器 Canvas 渲染器
DOM 节点数 每个形状都会变成节点,动画复杂时容易卡 整个动画在一个画布上绘制,节点数量很少
视觉清晰度 矢量缩放,任何分辨率都清晰 位图绘制,需要处理 DPR,可能边缘发虚(Lottie Canvas 通常会自动处理)
交互能力 方便绑定 DOM 事件,支持 CSS 操作 需要自己计算坐标,交互实现成本高
内存占用 节点多时容易触发重排,内存上升快 整体内存占用相对稳定,但单个图形大时也可能会高
适用场景 图标、按钮反馈、节点数少的简单动画 复杂形状、全屏动画、大量元素同时运动

从工程实践上说,我一般会把动画复杂度作为第一判断标准。如果形状数量在 500 以内,用 SVG 通常没有问题;但如果是一个有着上千路径的插画级动画,SVG 的 DOM 一定会拖垮页面。此时 Canvas 是更稳的选择。

lottie-web 切换渲染器很简单,甚至可以在运行时动态切换。不过我不建议一上来就动态切换,最好初始化时根据动画复杂度去选。

lottie.loadAnimation({
  container: document.getElementById('anim'),
  renderer: 'svg',
  loop: true,
  autoplay: true,
  path: 'animation-1.json',
  rendererSettings: {
    scaleMode: 'noScale',
    clearCanvas: true
  }
});

这里有一个容易踩的坑:用 Canvas 时如果 clearCanvas 设为 false,动画的每一帧都会叠加在前一帧上,看起来会像残影。但如果你需要做一个轨迹展现,反而可以利用这一点。通常我们保持默认 true 就好。

真正的性能陷阱:内存泄漏与调度

很多时候动画本身的复杂度并不高,但页面还是会卡,问题往往出在动画的生命周期管理上。尤其在一个后台系统或者中大型单页应用里,路由切换时如果没有正确销毁 Lottie 实例,动画会一直占用内存,节点也不会回收。

lottie.destroy() 会释放动画实例,但前提是你在正确的地方调用它。我见过很多代码只在组件卸载时调用 destroy,却没有处理定时器、事件监听和播放器内部的状态。如果你的单页应用框架是 React 或者 Vue,最好在生命周期函数里做清理。

另一个常见问题是不可见的动画仍然在播放。页面滚动到下方,Lottie 还在后台逐帧渲染,这显然是一种浪费。可以用 IntersectionObserver 监视容器是否可见,可见时播放,不可见时暂停。

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      anim.play();
    } else {
      anim.pause();
    }
  });
});
observer.observe(container);

还有一点容易被忽略:Lottie 动画解析 JSON 是同步的。如果你的 JSON 文件有几百 KB,又恰好在主线程上同步加载,首屏渲染的时间会被明显拖长。解决方案是把 JSON 放在异步请求里,或者用 Web Worker 预先解析。lottie-web 官方也支持传入 animationData,配合异步加载可以获得更好的体验。

再往前一步:更务实的优化路径

如果已经尝试了上面的方法,动画仍然跑得不够顺,那就需要从更宏观的角度来优化了。

第一种思路是拆。把一个大动画拆成几个小动画,分别控制它们的播放时机。比如一个引导页有三个阶段,你完全可以让设计师把它们导成三份 JSON,页面滚动到相应区域时再去加载对应动画。这样既避免了首屏一次加载过多,又能在交互上更灵活。

第二种思路是降级。对低端设备或弱网环境,可以暂时用静态图或者 CSS 动画替代 Lottie。做降级的时候不需要复杂的技术,你只需要在初始化时判断一下设备的硬件信息或者网络状态。比如用 navigator.deviceMemory 或者 network.effectiveType 做简单判断。

第三种思路是预加载。动画通常在用户操作后才会出现,比如点赞后的粒子效果。你可以在页面空闲的时候提前拉取 JSON 并解析好,等用户真正点击时直接实例化。这对交互体验的提升非常明显。

如果项目里有很多 Lottie 动画,我建议建立一套统一的加载和缓存机制。同一份 JSON 不要每次都被重新请求和解析,可以把 animationData 缓存到一个全局 Map 里,下次使用时直接传入。这能省掉大量重复的解析开销。

总结:Lottie 优化的链路是一个整体

回头看整条链路,你会发现在 AE 里少画一个不必要的图层,比在代码里做一百次微优化都有效。这也是我写这篇文章最想表达的一个观点:Lottie 动画优化不是某一段的事,而是设计、导出、渲染、运行时生命周期管理整个链条的系统工程。

有几个基本的判断标准,你可以直接用在项目里:如果动画复杂但只播放几次,请考虑 Canvas 或者视频;如果动画很轻但需要频繁交互,SVG 往往更好;如果页面里有十多个 Lottie 实例,优先做好懒加载和缓存,而不是逐个去抠渲染参数。

最后想说,Lottie 本身只是一套工具,它不会让烂动画变好,只会让好动画更高效地运行。把这套链路里的每一个环节都理顺了,你手上拿着的就不再是一个黑盒,而是真正可控的动效资产。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/952/

(0)
上一篇 53分钟前
下一篇 34分钟前

相关推荐