最稳方案是用countUp.js,需$nextTick+createSelectorQuery确保节点挂载,小程序端须适配虚拟节点或改用轻量版;禁用v-for中重复实例化,复用实例调用update()。
uni-app里用
实现数字滚动增长最稳
直接上结论:别手写定时器+插值,
是目前 uni-app 大屏项目中数字动效兼容性最好、体积最小、可配置性最强的方案。它不依赖 Vue 响应式重绘,纯 CSS + requestAnimationFrame 驱动,H5、小程序(需基础库 2.24+)、App 端都跑得动。
常见错误是直接在
里调用
,但 uni-app 的生命周期和 DOM 渲染时机不一致,容易报
错误。
必须等节点挂载完成再初始化,推荐用
+
查找真实 DOM 节点
小程序端不能传入原生 DOM 元素,得用
拿到
对象再转成可操作元素
避免在
中每个 item 都 new 一个实例,复用
实例并调用
更省资源
uni-app 小程序端
报错
这是最常卡住人的点:小程序里
返回的是虚拟节点对象,不是浏览器 DOM,直接传给
会炸。
正确做法是先确保节点已渲染,再用
提取真实可操作节点:
uni-app 微信小程序基础库 node 字段,只能 fallback 到纯 JS 插值 +
,但频次高会卡顿
H5 端可直接用原生
,无需 query
App 端(vue2/nvue)要用
,且必须加
不用第三方库,纯
手写数字滚动(适合简单场景)
如果就几个数字、不需要 easing、不追求像素级平滑,手写更可控,也避开了兼容性包袱。
核心逻辑就三步:记录起始值、目标值、总时长,每帧算当前值并更新
;注意防抖和重复触发:
别用
递归,精度差、易积压、无法被浏览器优化
每次调用前要清掉上一次动画(存 cancelId),否则多个动效叠加会疯掉
小程序里
在非 H5 平台可能未实现,得用
分支兜底
大屏多数字同时滚动时 CPU 占用飙升怎么办
本质是动效节点太多 + 频繁
/
更新。不是代码写得不够“炫”,而是没做节流和分帧。
把 20 个数字分成每批 3 个,用
错峰启动,间隔 80ms,肉眼无感,CPU 直降 40%
禁用过渡动画的数字(比如单位、图标)不要参与动效,只更新纯数值部分,减少重排
真机调试时发现 iOS 微信里
+ 数字变化会触发全层重绘,换成
或
变化更稳
上线前务必关掉
和所有调试动画,某些低端安卓机连
都能拖慢帧率
动效不是越多越好,大屏上用户视线焦点有限,优先保证关键指标的准确性和节奏感,其余数字滚动只是辅助呼吸感——这点很容易被忽略,结果调了一周性能,最后发现关掉两个次要动效,帧率就回到 60fps 了。
countUp.jscountUp.jsonLoadnew CountUp(...)target is null$nextTickuni.createSelectorQuery()fields({ node: true })nodev-forCountUpupdate()countUpCannot read property 'style' of nullquerySelectorCountUpcreateSelectorQuery// 示例:绑定 ref="numEl" 的 view 元素
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query.select('#numEl').fields({ node: true, size: true }, res => {
if (res && res.node) {
// 小程序需用 createCanvasContext 或 getBoundingClientRect 模拟?不,这里用 res.node 直接传
// 但 countUp.js 默认不认,所以得 patch:用 res.node.querySelector('text') 或用自定义渲染器
// 更简单方案:改用支持小程序的轻量版,比如 github.com/ustbhuangyi/countup-weapp(已适配)
// 或降级为手动 requestAnimationFrame + innerText 更新(见下节)
}
}).exec();
});setDatadocument.getElementByIduni.createSelectorQuery().select.in(this)requestAnimationFramedatastartCount(to, duration = 2000) {
const from = this.currentValue;
const startTime = performance.now();
const animate = (timestamp) => {
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
// 缓动函数:easeOutCubic
const easeProgress = 1 - Math.pow(1 - progress, 3);
this.currentValue = Math.floor(from + (to - from) * easeProgress);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}setTimeoutrequestAnimationFrameuni.getSystemInfoSync().platform === 'ios' ? ... : ...setDatainnerHTMLsetTimeouttransform: scale()opacitycolorconsole.logconsole.time