小程序的交互动画这两年卷得厉害从最简单的按钮反馈到弹窗里的加载小人、营销页的引导动效、会员开通的礼花效果产品经理张口就是要那种丝滑的。真到落地环节用 WXSS 的 animation 写几个节点位移还行一旦遇到设计师在 AE 里做出来的几十上百个图层、带缓动曲线和遮罩的动画纯手写基本就是自虐。这时候 lottie 就派上用场了设计师用 AE 加 Bodymovin 插件导出 JSON前端拿 lottie-miniprogram 在小程序里渲染一套流程下来动画还原度能做到很高也不需要前端去逐帧抠参数。这篇内容聊的就是微信小程序使用 lottie 动画的完整链路从前置条件、依赖安装、核心 API、实操代码到性能调优和踩坑记录面向的是有点小程序基础、但没怎么碰过 Lottie 的开发者也照顾到已经在用但遇到帧率或者内存问题的朋友。1. 为什么要用 Lottie 而不是原生动画方案选型拆解1.1 小程序自带动画能力的边界在哪先说清楚小程序原生能做哪些动画。WXSS 层面有transition和keyframes配合transform、opacity这类合成属性做按钮缩放、淡入淡出、位移动画是完全够用的而且走的是渲染层合成性能开销小。问题出在两个地方一是复杂路径动画比如一个带描边的图标沿着曲线生长用 WXSS 基本写不出来得靠 SVG path 配合stroke-dasharray硬凑改一次动效就要重算一次长度二是多元素时间轴编排设计师给的时间轴是第 0 帧圆点弹出、第 8 帧线条展开、第 20 帧文字逐字出现、第 45 帧整体上移用 CSS animation-delay 去对齐几十个元素的时间点维护成本高得离谱改一帧全盘重算。再看wx.createAnimation和 WXS 响应事件这两个更适合做手势跟随类的即时反馈比如拖动卡片、下拉回弹它们解决的是交互驱动而不是时间轴驱动的问题。所以结论很清楚简单交互动效用原生复杂的、设计师主导的、帧数密集的动效交给 Lottie。还有一个容易被忽略的点是动画的一致性。同一个动效在 iOS 和 Android 上因为渲染引擎差异WXSS 动画偶尔会出现细微的时序偏差尤其在低端安卓机上掉帧明显。Lottie 因为是按帧数据渲染到 canvas节奏由 JS 主动驱动反而更容易做到两端表现接近——当然代价是 JS 线程的压力变大这点后面会细说。1.2 lottie-miniprogram 到底解决了什么问题lottie-web 是业界通用的 Lottie 渲染库支持 SVG、Canvas、HTML 三种渲染器。小程序里没有 DOM也没有标准的 SVG 元素直接引入 lottie-web 是跑不起来的。lottie-miniprogram做的事情本质上是把 lottie-web 内部依赖浏览器环境的几个关键模块做了小程序适配一是把 XMLHttpRequest 换成了wx.request这样 JSON 资源能通过网络加载二是把document.createElement(canvas)这类操作替换成小程序提供的 canvas 2d 节点三是把requestAnimationFrame桥接到 canvas 节点自带的帧回调上。做完这三件事之后lottie-web 的 Canvas 渲染器就能在小程序里正常工作了。要注意lottie-miniprogram是腾讯官方团队维护的适配层底层还是 lottie-web 的 canvas 渲染逻辑所以 lottie-web 支持的 AE 特性它基本都支持不支持的它也照样不支持。这个认知很重要很多人第一次用的时候会以为小程序的 Lottie 是阉割版其实阉割的是渲染器只有 canvas没有 svg特性支持度取决于 lottie-web 版本。对比一下其他方案手写 Canvas 逐帧绘制等于自己实现一个动画播放器工作量巨大用 GIF体积大、色带明显、没法动态改颜色用 APNG 或 WebP 动图小程序支持有限且同样无法交互控制。综合下来Lottie 在小程序里目前就是复杂 JSON 动画的最优解没有之一。1.3 什么场景该用什么场景不该用不能无脑上 Lottie我列几个实际判断标准。适合用的场景营销活动的引导页动效、空状态插画动画、加载 Loading尤其是品牌化的定制 Loading、会员/成就解锁的庆祝动画、新手引导的手势提示动画。这些场景的共同点是动画复杂、复用度高、由设计主导。不太适合的场景列表项里的循环动画。比如聊天列表每个头像旁边都有一个小动画一屏十几个实例canvas 数量一多内存直接爆掉这种情况建议用静态图加 CSS 的轻微缩放代替。再比如纯颜色变化的过渡动画用 Lottie 就是杀鸡用牛刀反而增加体积。另外一个硬指标是包体积。小程序主包限制 2MB一个中等复杂度的 Lottie JSON 大概 30KB 到 200KB图片资源多的能到 1MB 以上。如果主包已经很紧张JSON 就别放本地了走 CDN 或者放分包这点在第三部分会展开讲。2. 环境准备与依赖安装从零把库跑起来2.1 基础库版本与 canvas 2d 的前置条件lottie-miniprogram依赖小程序的 canvas 2d 接口这个接口是基础库 2.9.0 才开始提供的。所以第一件事是确认你项目的最低基础库版本在开发者工具的详情 - 本地设置 - 调试基础库里能看到当前调试版本在app.json里也可以通过lazyCodeLoading等相关配置间接影响。更稳妥的做法是在project.config.json里设置libVersion把它固定在一个 2.9.0 以上的版本。还有一个容易踩的坑app.json里如果没有开启lazyCodeLoading: requiredComponents虽然不影响 Lottie 运行但会影响整体启动性能。这个配置和 Lottie 没有直接关系不过很多新项目模板默认开启了如果你从老项目迁移过来发现没开顺手补上没坏处。最关键的其实是不要用旧版 canvas。小程序历史上有一套老的canvas canvas-idxxx用的是wx.createCanvasContext这套 API 已经废弃lottie-miniprogram完全不支持。你必须用canvas type2d idxxx这种新写法通过SelectorQuery.node()拿到真实的 canvas 节点对象。这一点新手特别容易搞混因为网上很多老教程还在讲createCanvasContext。2.2 npm 安装与构建流程lottie-miniprogram是通过 npm 分发的所以项目得先支持 npm。流程如下先在项目根目录执行npm init -y生成package.json然后安装依赖。npm install lottie-miniprogram --save装完之后你会发现根目录多了node_modules但小程序运行时读的是miniprogram_npm目录所以还需要在微信开发者工具里点工具 - 构建 npm。构建成功后miniprogram_npm/lottie-miniprogram就会出现这时候才能import。如果你用的是 TypeScript 项目类型声明可能没有内置可以在项目里加一个简单的declare module lottie-miniprogram声明文件或者直接require引入然后自己断言类型不影响运行。注意npm init -y生成的 package.json 里如果 name 包含中文或大写字母构建 npm 时可能报错记得改成合法的小写包名。构建 npm 之后还有一步经常被漏掉在开发者工具里勾选使用 npm 模块新版工具已经默认支持但老版本需要在详情里手动开。如果import lottie from lottie-miniprogram报找不到模块八成就是构建没做或者没勾选。2.3 目录结构与最小可运行骨架一个干净的最小项目结构大概是这样的project/ ├── miniprogram/ │ ├── pages/ │ │ └── index/ │ │ ├── index.js │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.json │ ├── static/ │ │ ── lottie/ │ │ └── loading.json │ ├── app.js │ ├── app.json │ └── app.wxss ├── miniprogram_npm/ │ └── lottie-miniprogram/ ├── node_modules/ ├── package.json └── project.config.json把 Lottie 的 JSON 放static/lottie目录下通过相对路径require进来是最省事的方式因为不涉及网络请求也不用配域名。缺点是占包体积好处是首屏无延迟、离线可用。放本地还是放 CDN后面会专门讲取舍逻辑。app.json里不需要为 Lottie 做任何特殊配置因为它不是自定义组件只是一个 JS 库。真正需要在页面里配的只有页面本身的 WXML 结构。3. 核心 API 与渲染原理拆解3.1 lottie.setup 与 loadAnimation 各参数的含义lottie-miniprogram暴露的 API 非常少核心就两个setup和loadAnimation。setup(canvas)的作用是把 canvas 节点上挂载的requestAnimationFrame和cancelAnimationFrame注册到全局这样 lottie-web 内部的动画循环才能跑起来。这一步必须在loadAnimation之前调用而且每个页面只需要调用一次。它接收的就是通过SelectorQuery.node()拿到的 canvas 节点对象。loadAnimation(options)是真正的加载入口常用参数如下参数类型作用备注pathstringJSON 网络地址需要配置 request 合法域名animationDataobject直接传入 JSON 对象配合本地 require 使用loopboolean/number是否循环/循环次数true 为无限循环autoplayboolean是否自动播放默认 truerendererSettings.contextCanvasRenderingContext2Dcanvas 上下文必填否则渲染不出来rendererSettings.clearCanvasboolean每帧是否清空画布默认 true正常不要改namestring动画实例名用于lottie.getAnimationByNameinitialSegment[number, number]初始播放区间用于分段播放path和animationData二选一。用path要注意小程序的网络请求域名白名单因为 lottie-miniprogram 内部是用wx.request拉 JSON 的如果你没在开发设置 - 服务器域名里配置 request 合法域名真机上会直接失败而开发者工具里勾了不校验合法域名却又能跑这是个经典的工具能跑真机白屏问题。用animationData就简单多了本地require一个 JSON直接传对象进去没有网络请求也就没有域名问题。缺点是打包体积。我的建议是动画 JSON 小于 150KB 放本地超过就上 CDN 配置域名。3.2 canvas 2d 与 DPR 缩放的正确姿势这是整个流程里最容易出问题的一环。小程序的 canvas 2d 节点canvas.width和canvas.height是物理像素而它在页面上占的位置由 WXSS 里的宽高决定这两个是分离的。同时lottie 内部渲染用的坐标系是 CSS 像素。正确做法是把 canvas 的物理尺寸设成 CSS 尺寸乘以设备的pixelRatio然后对 context 做一次scale(dpr, dpr)这样 lottie 按 CSS 尺寸渲染出来的内容才不会糊也不会被裁切。const dpr wx.getWindowInfo().pixelRatio const cssWidth 200 const cssHeight 200 canvas.width cssWidth * dpr canvas.height cssHeight * dpr const context canvas.getContext(2d) context.scale(dpr, dpr)如果忘了乘 dpr在高分屏大多 iPhone 是 3 倍部分安卓是 2.75 倍上动画会明显发虚边缘毛刺严重。如果乘了 dpr 但忘了scale动画内容只会占左下角四分之一因为渲染坐标系还是 CSS 尺寸而画布已经放大了一倍多。wx.getWindowInfo()是较新的 API如果你的基础库版本偏低用wx.getSystemInfoSync().pixelRatio也完全没问题只是前者官方推荐用在新项目里。注意pixelRatio在 iPad 上可能是 2在某些折叠屏上会是 3.5 甚至更高。写死 dpr 是典型的埋雷操作一定要动态读取。还有一点canvas 的 WXSS 宽高和上面 JS 里的cssWidth/cssHeight必须一致否则会出现内容被拉伸的问题。最稳妥的做法是把尺寸抽成常量两边都引用同一个值或者用SelectorQuery的boundingClientRect读实际尺寸。3.3 JSON 资源加载path、animationData 与 CDN 取舍前面提了两种加载方式这里讲清楚背后的取舍逻辑。path方式的优点是包体积为零JSON 更新不用发版改完 CDN 上的文件用户下次打开就是新版。缺点是首屏有网络延迟弱网下动画会明显晚出现需要配置 request 合法域名CDN 假设挂了动画就废了。所以适合那些体积大、更新频繁、允许延迟出现的动画比如节日活动的氛围动效。animationData方式的优点是零延迟、可离线、无域名依赖。缺点是占包体积更新必须重新提审发版。适合小尺寸的品牌 Loading、空状态插画这类基础设施级的动画。还有一种混合方案是很多团队在用的把核心动画放本地保证首屏把可选的、体积大的动画放 CDN 按需加载。加载的时候用wx.request先拿到 JSON 文本再JSON.parse之后作为animationData传给 lottie这样既不用配域名如果你的请求走的是自己后端且配好了域名又能控制加载时机。CDN 侧还有一个实践要点给 JSON 开 gzip 或者 br 压缩。Lottie 的 JSON 是纯文本、重复度极高压缩率通常能到 70% 以上一个 200KB 的 JSON 压完可能就 50KB 出头首屏速度提升非常明显。同时记得给 CDN 配上合适的缓存策略max-age设长一点文件名带 hash 做版本控制。4. 实操落地一个完整的动画页面4.1 WXML 与 WXSS 写法WXML 部分非常简洁一个 canvas 节点就够了关键属性是type2d和id。view classwrap canvas type2d idlottie-canvas classlottie-canvas /canvas view classbtn-group button sizemini bindtaphandlePlay播放/button button sizemini bindtaphandlePause暂停/button button sizemini bindtaphandleDestroy销毁/button /view /viewWXSS 里给 canvas 一个明确的宽高单位用 rpx但要注意和 JS 里的 CSS 像素换算。小程序的 rpx 是按 750 设计稿宽度等比缩放的在 JS 里拿到的是 px所以简单起见canvas 的宽高直接用 px 写死更稳妥或者在 JS 里通过boundingClientRect读实际渲染尺寸。.wrap { display: flex; flex-direction: column; align-items: center; padding: 40rpx 0; } .lottie-canvas { width: 200px; height: 200px; } .btn-group { margin-top: 40rpx; display: flex; gap: 20rpx; }这里我特意用 px 而不是 rpx 来定义 canvas 尺寸原因是 Lottie 的动画本身有个设计稿尺寸JSON 里的w和h字段如果 canvas 的宽高比和动画不一致会出现拉伸或者留白。用 px 固定尺寸配合 lottie 的等比缩放表现最可控。4.2 JS 逻辑初始化、播放、暂停、销毁完整的页面逻辑如下import lottie from lottie-miniprogram const ANIMATION_DATA require(../../static/lottie/loading.json) Page({ data: {}, ani: null, canvasNode: null, onReady() { this.initLottie() }, initLottie() { const query wx.createSelectorQuery().in(this) query .select(#lottie-canvas) .fields({ node: true, size: true }) .exec((res) { if (!res || !res[0] || !res[0].node) { console.error(canvas 节点获取失败) return } const canvas res[0].node const cssWidth res[0].width const cssHeight res[0].height const dpr wx.getWindowInfo().pixelRatio canvas.width cssWidth * dpr canvas.height cssHeight * dpr const context canvas.getContext(2d) context.scale(dpr, dpr) lottie.setup(canvas) this.canvasNode canvas this.ani lottie.loadAnimation({ animationData: ANIMATION_DATA, loop: true, autoplay: true, rendererSettings: { context, }, }) this.ani.addEventListener(loopComplete, () { // 每一轮循环结束的回调 }) }) }, handlePlay() { this.ani this.ani.play() }, handlePause() { this.ani this.ani.pause() }, handleDestroy() { if (this.ani) { this.ani.destroy() this.ani null } }, onHide() { this.ani this.ani.pause() }, onUnload() { this.handleDestroy() }, })几点说明。fields({ node: true, size: true })比node()更实用因为它一次性把节点和渲染尺寸都拿到省了再查一次boundingClientRect。in(this)是给自定义组件场景准备的如果这个页面本身是组件不加in(this)会查不到节点。onHide里暂停是必须做的。小程序退到后台时canvas 的帧回调会停但 lottie 内部的计时状态可能残留回到前台时容易出现动画跳帧或者一次性快进很多帧的诡异现象。主动pause()再在onShow里play()表现会稳定得多。onUnload里destroy()也是必须的。destroy()会清掉 lottie 内部持有的动画实例、事件监听和帧定时器不做这步的话来回进出页面几次内存就会明显上涨尤其在安卓低端机上容易触发卡顿。4.3 交互进阶进度控制、分段播放、事件回调基础的播放暂停之外lottie-miniprogram还支持不少实用能力。进度控制用的是goToAndStop(value, isFrame)和goToAndPlay(value, isFrame)。value是帧号或者进度百分比0 到 1isFrame为 true 时按帧号解释。这个能力在做手势拖拽控制动画进度的时候特别有用比如用户滑动屏幕动画跟着手指走松手后再play()继续。实现思路是监听touchmove拿到滑动的百分比转成动画进度传给goToAndStop。分段播放靠的是initialSegment参数或者playSegments(segments, forceFlag)。比如一个按钮动画分进入和循环两段第 0 到 30 帧是进入第 31 到 60 帧是循环就可以用playSegments([[0, 30]], true)先播进入在complete事件里再playSegments([[31, 60]], true)接循环。这套东西做复杂状态机动画非常顺手。事件回调方面常用的有这几个data_ready是 JSON 加载完成DOMLoaded在 canvas 渲染器下也会触发complete是动画播完一轮非循环模式loopComplete是每轮循环结束。注意complete和loopComplete的区别循环模式下调complete是永远不会触发的这点很多人在群里问过。setSpeed(speed)可以动态改速度setDirection(dir)可以反向播放dir 传 -1配合setSpeed负值也能实现倒放效果用来做展开再收回的动画很省事不用让设计师单独做一份反向动画。还有一个冷门但好用的 API 是getDuration()返回动画总时长秒做进度条或者超时兜底的时候能派上用场。5. 性能优化与常见问题排查实录5.1 内存与帧率优化清单Lottie 在小程序里的性能瓶颈主要在 JS 线程和 canvas 绘制。下面这几条是我踩过坑之后固定下来的优化习惯。第一控制实例数量。一个页面最多同时跑 1 到 2 个 Lottie超过 3 个就要警惕。如果确实需要多个动画考虑做成一张 JSON 里包含多个图层统一调度这样只占一个 canvas 和一个实例开销反而更小。第二善用onHide暂停和onUnload销毁前面已经强调过。另外在onShow恢复时如果不是当前活动页面宁可延迟一拍再play()避免页面切换瞬间堆叠多个动画同时启动。第三控制动画的复杂度。这条其实要跟设计师沟通。Bodymovin 导出的图层数、蒙版数、路径点数直接决定每帧的绘制开销。一个 300 个图层的 JSON在低端安卓上掉帧是必然的。实践下来单个动画控制在 50 到 80 个图层以内帧率一般能稳住。另外尽量避免在 Lottie 里用大面积的模糊和阴影效果这类是纯粹的算力黑洞。第四注意 canvas 尺寸不要开太大。有人图省事canvas 开个 750px 乘 750px再乘 3 倍 dpr画布实际是 2250 的物理像素每帧的绘制像素量巨大。按实际展示尺寸开能用小尺寸就不用大尺寸。第五考虑降级策略。低端设备上可以通过wx.getDeviceInfo()判断机型或者内存等级命中低端规则时直接不加载 Lottie展示静态图兜底。这个策略看起来粗暴但在真实的大促场景里能救不少机型。优化项建议值说明单页实例数1 到 2 个超过 3 个必掉帧单动画图层数50 到 80 层以内300 层以上低端机必卡canvas 物理像素不超过 1200 像素宽再大收益递减开销剧增JSON 体积本地不超过 150KB超过走 CDN循环动画帧率30fps 可接受60fps 在低端机偏重5.2 常见报错与速查表下面这几个问题是我和团队实际遇到频率最高的整理成表方便对照排查。现象可能原因排查方向动画完全不显示canvas 2d 未正确取到节点检查type2d、id是否匹配、in(this)动画显示在左下角一小块未做 dpr 缩放处理检查canvas.width和context.scale动画发虚、边缘毛刺canvas.width未乘 dpr补上pixelRatio乘法工具能跑真机白屏JSON 走网络但域名未配检查 request 合法域名设置JSON 加载报错路径错误或 JSON 格式非法用JSON.parse验证文件iOS 上颜色偏暗canvas 色彩空间差异检查 JSON 里的颜色模式页面返回后动画重影未 onUnload 销毁补上destroy()后台切回后动画跳帧未 onHide 暂停补上pause/play多个动画只有一个能动全局 rAF 被覆盖每个 canvas 单独setup部分效果丢失AE 表达式或效果不支持导出前转换表达式为关键帧其中多个动画只有一个能动这个坑特别隐蔽。因为setup()会把最后一个 canvas 的requestAnimationFrame覆盖到全局如果你在同一个页面里 setup 了两个 canvas第二个会把第一个的帧回调顶掉表现就是只有第二个动画在跑。解决办法是不用setup的全局覆盖或者干脆一个页面只用一个 canvas把多个动画合成到一张 JSON 里。另外部分效果丢失这个问题根源在 AE 端。lottie-web 不支持 AE 的表达式Expressions设计师如果用了表达式驱动的动画导出后这部分会变成静止的。解决办法是在 AE 里把表达式烘焙成关键帧再导出。同理一些旧的混合模式和部分内置效果比如特定的位移模糊导出后也会异常导出前需要用 Bodymovin 的预览功能确认一遍。5.3 导出环节的坑Bodymovin 设置前端和设计师的配合里导出规范不对是返工率最高的环节。这里给出几条我在项目里固定下来的规范。第一合成尺寸尽量用偶数常见是 750x750 或者 375x375。奇数尺寸在小尺寸位图上容易出现半像素偏移渲染出来边缘会有细微抖动。第二导出时在 Bodymovin 设置里勾选Glyphs文字转字形这样文字会被转成矢量路径不依赖系统字体。否则用户设备上没装设计师用的字体时文字会变成默认字体位置和样式全乱。第三图片资源建议转成 base64 内嵌或者统一用 CDN 地址。用相对路径引用本地图片在小程序里是找不到的因为小程序没有文件系统概念。base64 内嵌会让 JSON 体积膨胀所以图片多的情况建议走 CDN。第四不要用时间重映射Time Remapping。这个特性在 lottie-web 上支持不完整路径跟随类的效果也建议手动拆成关键帧。第五导出后一定要用 Lottie 官方预览工具或者在线编辑器先看一遍确认帧率、颜色、层级都对再交给前端接入。这个环节花五分钟能省掉后面两小时的联调。提示如果设计师不愿意配合规范退而求其次的方案是前端拿到 JSON 后用animationData动态改写里面的部分字段比如替换颜色值实现主题换肤但这属于补丁式操作层级和解构成本高还是提前约定规范更划算。5.4 换肤与动态改色的实操思路最后补一个很实用的技巧Lottie 动画支持动态改色这在多主题的小程序里很常见。做法是遍历animationData.layers找到类型为形状图层的元素修改里面的c.k颜色值。颜色值是归一化后的 RGB 数组比如纯红是[1, 0, 0, 1]需要把十六进制颜色转成这个格式。function hexToLottieColor(hex) { const r parseInt(hex.slice(1, 3), 16) / 255 const g parseInt(hex.slice(3, 5), 16) / 255 const b parseInt(hex.slice(5, 7), 16) / 255 return [r, g, b, 1] } function applyTheme(animationData, colorArr) { const cloned JSON.parse(JSON.stringify(animationData)) cloned.layers.forEach((layer) { if (layer.ty 4 layer.shapes) { layer.shapes.forEach((shape) { if (shape.it) { shape.it.forEach((item) { if (item.ty fl || item.ty st) { item.c.k colorArr } }) } }) } }) return cloned }这段逻辑要注意几个点一是必须深拷贝原始 JSON 再改直接改原对象会污染后续复用二是形状图层ty 4里的结构可能嵌套多层it下面还可能有it实际项目里建议写个递归函数三是填充fl和描边st都要处理只改填充的话描边颜色还是旧的视觉上会很怪。这套方案在做暗色模式和品牌色切换的时候特别省事同一个 JSON 一次加载切换主题时destroy旧实例用改色后的animationData重新loadAnimation一次即可动画资源复用了视觉却完全不同。我个人在实际项目里的体会是Lottie 在小程序里的落地难点从来不在代码本身API 就那么几个半天就能摸熟。真正决定成败的是两件事一是和设计师约定好导出规范别让带表达式的 AE 工程流到前端二是把生命周期管住该暂停暂停、该销毁销毁别让一个看起来很轻的动画在用户手机上悄悄吃掉内存。这两件事做扎实了剩下的都是水到渠成。