把 Framer Motion 搬进微信小程序:三个让动画哑掉的反直觉坑
设计师交付了一份用 React + Framer Motion 搭的高保真原型:数字滚动增长、Emoji 周期摆动、点击卡片有弹性缩放。产品体验极好。然后我开始把它迁移到微信小程序原生实现——没有 requestAnimationFrame,没有 hooks,只有 WXSS @keyframes 和 setInterval。
结果动画一个都没出来。页面一片静止。
现象
三处动效全部失效:
- Emoji 摆动:
@keyframes写好了,挂上去,没有任何动画。 - 卡片点击反馈:
:active触发了(背景色变了),但scale和rotate完全不动。 - 数字滚动:
setInterval里setData频繁触发,页面卡,但数字跳跃而非流畅计数。
每个问题都单独沉默地失败,没有任何报错。
根因
坑一:<text> 是 inline,transform 对 inline 元素无效
WXML 里的 <text> 默认渲染为 inline 元素,就像 HTML 的 <span>。CSS transform 对 inline 元素不生效——这是 CSS 规范行为,不是小程序 bug。
Emoji 往往写在 <text> 里。给它加 animation: emojiWobble 1.5s infinite 后,动画类确实被挂上了,但 transform: rotate() 对 inline 元素没有渲染效果,所以 Emoji 纹丝不动,控制台零报错。
坑二::active 在 <navigator> 上不响应
微信小程序 :active 伪类可以作用于 <view>,但不响应 <navigator> 元素。如果卡片外层套了 <navigator> 做跳转,:active { transform: scale(0.88) } 永远不会被触发。需要改用 hover-class 属性。
坑三:countUp 的 setData 节拍太密,导致桥调用堆积
将 useCountUp(target, 1200ms) 的每帧更新直接搬过来,用 setInterval(fn, 16) 模拟 60fps,1200ms 内产生 75 次 setData。小程序的 JS → Renderer 通信桥在频繁 setData 下会堆积,表现为数字跳跃感强、页面其他元素短暂卡顿。
正解
修 Emoji 动画:加 display: inline-block
/* WXSS */
.emoji-wobble {
display: inline-block; /* 关键:让 transform 生效 */
animation: emojiWobble 1.5s ease-in-out 1s infinite;
}
@keyframes emojiWobble {
0%, 100% { transform: rotate(0deg); }
15% { transform: rotate(14deg); }
35% { transform: rotate(-8deg); }
55% { transform: rotate(14deg); }
75% { transform: rotate(0deg); }
}bounce 版同理:
.emoji-bounce {
display: inline-block;
animation: emojiBounce 2s ease-in-out infinite;
}
@keyframes emojiBounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6rpx); }
}所有需要 transform 的 <text> 或其他 inline 元素,一律加 display: inline-block,这是让 CSS transform 生效的前提。
修点击反馈:<view> 用 :active,<navigator> 用 hover-class
/* view 卡片 */
.card {
transition: transform 0.2s ease;
}
.card:active {
transform: scale(0.88) rotate(-2deg);
}<!-- navigator 包裹时改用 hover-class -->
<navigator url="/pages/detail/index"
hover-class="card-pressed"
hover-stay-time="150">
<view class="card">...</view>
</navigator>.card-pressed {
transform: scale(0.88) rotate(-2deg);
}修 countUp:降到 30 步,走 easeOutCubic
_countUp(fields, duration) {
var that = this
duration = duration || 1200
var steps = 30 // 每步 ~40ms,比 60fps 稀疏 10 倍,setData 成本可控
var interval = duration / steps
var step = 0
var targets = {}
for (var k in fields) targets[k] = fields[k]
if (that._countUpTimer) clearInterval(that._countUpTimer)
that._countUpTimer = setInterval(function () {
step++
var progress = Math.min(step / steps, 1)
var eased = 1 - Math.pow(1 - progress, 3) // easeOutCubic
var update = {}
for (var k in targets) update[k] = Math.round(eased * targets[k])
that.setData(update)
if (progress >= 1) {
clearInterval(that._countUpTimer)
that._countUpTimer = null
}
}, interval)
},30 步在视觉上与 60fps 无明显区别(人眼 countUp 感知靠加速度曲线,不靠帧率),但 setData 次数降至原来的 40%,桥调用压力显著减轻。
如果页面同时有积分和金币两个数字需要滚动,把它们合并进同一个 setData({ displayPoint, displayCoin }) 调用,而不是分两个 setInterval,可以再减一半桥调用。
别忘了在 onHide / onUnload 清掉 timer,否则页面切走后定时器还在悄悄 setData:
onUnload() {
if (this._countUpTimer) {
clearInterval(this._countUpTimer)
this._countUpTimer = null
}
},一句话外卖
微信小程序里
<text>是 inline,CSStransform对它静默无效;:active对<navigator>也静默无效——凡是动画哑掉零报错,先查元素的 display 和标签类型。