requestAnimationFrame + 数值插值可实现轻量可控的数字滚动动画;因与屏幕刷新同步、页面失焦自动暂停、不累积延迟,比 setInterval 更顺滑省资源,且需保留小数插值再四舍五入显示。

直接用 requestAnimationFrame + 数值插值就能实现轻量、可控的数字滚动动画,不需要引入任何库。
为什么不用 setInterval 做数字递增动画
setInterval 容易因页面失焦、JS 执行阻塞导致计时不准,数字跳变或卡顿;而 requestAnimationFrame 与屏幕刷新同步,更顺滑,且在标签页不可见时自动暂停,省资源。
- 浏览器可能丢帧,但不会“累积延迟”,数值始终按真实经过时间线性插值
- 避免用
parseInt或Math.floor强制取整后突变——应保留小数位做视觉过渡,最后再四舍五入显示 - 若用 CSS
transition动画数字字符串(如 “123” → “456”),本质是文本替换,无法实现平滑递增感
核心函数:用 requestAnimationFrame 实现带缓动的数字滚动
关键不是“每帧加 1”,而是从起始值 start 按时间比例算出当前应显示的中间值:
function animateNumber(el, start, end, duration = 2000) {
const startTime = performance.now();
const ease = t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // easeInOutQuad
function step(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = ease(progress);
const current = start + (end - start) * eased;
el.textContent = Math.round(current); // 最终显示取整,但计算全程用浮点
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}调用:animateNumber(document.getElementById('counter'), 0, 12345, 1500)
立即学习“前端免费学习笔记(深入)”;
处理多位数字对齐和千分位逗号问题
直接 textContent = Math.round(current) 会丢失格式;但频繁加逗号又影响性能。推荐在每次渲染时用 Intl.NumberFormat 格式化(现代浏览器支持好,且可复用):
- 提前创建格式器:
const formatter = new Intl.NumberFormat('en-US') - 渲染时:
el.textContent = formatter.format(Math.round(current)) - 不要在循环里反复
new Intl.NumberFormat(),避免内存开销 - 若需兼容 IE,改用正则手动加逗号(
String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ',')),但注意负数和小数点位置
多个计数器同时启动时的注意事项
每个动画必须有独立的 startTime 和闭包状态,不能共用变量。常见错误是把所有计数器塞进同一个 for 循环却没做闭包隔离:
// ❌ 错误:i 被共享,最终全部从最后一个值开始
for (let i = 0; i < els.length; i++) {
animateNumber(els[i], 0, values[i]);
}
<p>// ✅ 正确:用立即执行函数或 for...of + const
els.forEach((el, i) => animateNumber(el, 0, values[i]));另外,如果页面加载时计数器不在视口内,建议监听 IntersectionObserver,等元素进入可视区再触发动画,避免无谓计算。
真正难的不是让数字动起来,而是控制好「起始时机」「结束精度」和「格式稳定性」——尤其是当 end 是动态接口返回值、且可能为小数或负数时,Math.round 和格式化逻辑得提前兜底。


















