4、JavaScript动画基础:requestAnimationFrame、setTimeout/setInterval对比、性能监控、简单缓动函数实现

聊到跨平台动画,JavaScript 是绕不开的基础。不管你是用 React Native、Flutter 还是 Web 开发,底层动画调度逻辑其实都差不多。今天我就带你把这几个核心概念彻底搞明白。

4.1 为什么不用 setTimeout 做动画?

很多新手喜欢用 setTimeoutsetInterval 做动画。代码写起来简单,对吧?

let left = 0;
const interval = setInterval(() => {
  left += 5;
  element.style.transform = `translateX(${left}px)`;
  if (left >= 300) clearInterval(interval);
}, 16);

嗯,这里要注意。这段代码看起来每 16ms 执行一次,差不多 60fps。但实际运行起来,你会发现动画一卡一卡的。

为什么会这样?

因为 setTimeoutsetInterval 根本不关心浏览器渲染周期。它们只保证「至少延迟这么多毫秒后执行」,但不保证「在帧渲染前执行」。我在项目中遇到过这种情况:一个简单的滚动动画,用 setInterval 写完后在低端手机上像幻灯片一样。

说白了,这两个 API 的精度不够。JavaScript 是单线程的,如果主线程有其他任务在跑,你的回调就会被推迟。更糟糕的是,即使回调准时执行了,浏览器也可能因为布局计算、样式重绘等原因错过这一帧。

避坑指南: 我曾经用 setInterval 做倒计时动画,结果页面切到后台再切回来,动画直接「瞬移」到结束位置。因为浏览器为了省电,后台标签页会降低定时器频率。

4.2 requestAnimationFrame:浏览器亲儿子

requestAnimationFrame(简称 rAF)才是做动画的正确姿势。它告诉浏览器:「嘿,下次你要刷新屏幕的时候,记得叫我一声。」

let left = 0;
function animate() {
  left += 5;
  element.style.transform = `translateX(${left}px)`;
  if (left < 300) {
    requestAnimationFrame(animate);
  }
}
requestAnimationFrame(animate);

你想想看,rAF 的回调是在浏览器准备渲染下一帧之前执行的。这意味着你的动画更新和浏览器的绘制周期是同步的。不会出现「更新了但没渲染」或者「渲染了但没更新」的尴尬情况。

我个人习惯用 rAF 做所有需要连续更新的场景——动画、游戏循环、甚至一些高频的 UI 状态同步。

核心区别:
  • setTimeout/setInterval:不管浏览器死活,到点就执行
  • requestAnimationFrame:跟着浏览器节奏走,页面不可见时自动暂停

4.3 性能对比:一张表说清楚

特性 requestAnimationFrame setTimeout/setInterval
执行时机 浏览器渲染前一帧 延迟到期后尽快执行
帧率控制 自动匹配屏幕刷新率 手动设置间隔,容易错帧
后台行为 自动暂停(省电) 继续执行(浪费性能)
精度 高,与 VSync 同步 低,受事件队列影响
CPU/GPU 占用 优化,页面不可见时停止 持续占用,即使页面不可见

我建议你记住一句话:能用 rAF 就别用定时器。除非你只是想做「每隔几秒执行一次」的轮询,否则动画场景一律用 rAF。

4.4 性能监控:别靠感觉,靠数据

做动画最怕什么?怕你觉得流畅,用户觉得卡。所以性能监控是必修课。

最简单的办法是计算帧率(FPS)。我们可以用 rAF 自己实现一个 FPS 计数器:

let frameCount = 0;
let lastTime = performance.now();

function monitorFPS() {
  frameCount++;
  const now = performance.now();
  const delta = now - lastTime;
  
  if (delta >= 1000) {
    const fps = Math.round((frameCount * 1000) / delta);
    console.log(`当前帧率:${fps} FPS`);
    frameCount = 0;
    lastTime = now;
  }
  
  requestAnimationFrame(monitorFPS);
}
requestAnimationFrame(monitorFPS);

我在项目中遇到过一个问题:动画在开发工具里跑得很流畅,但真机上一卡一卡的。后来加上 FPS 监控才发现,原来是某个第三方库在每帧都触发了强制回流。

小技巧: 除了 FPS,还可以监控帧耗时(frame duration)。如果单帧耗时超过 16ms(60fps 对应的阈值),说明这一帧「超时」了,需要优化。

4.5 简单缓动函数实现

动画光有线性移动是不够的。你想想看,现实世界哪有东西是匀速运动的?

缓动函数(easing function)就是用来模拟这种「加速-减速」效果的。本质上就是一个数学公式,把时间进度映射成位置进度。

先看一个最简单的缓动函数——ease-out(减速退出):

function easeOut(t) {
  return 1 - Math.pow(1 - t, 3);
}

// 使用示例
const duration = 1000; // 动画时长 1 秒
const startTime = performance.now();
const startPos = 0;
const endPos = 300;

function animateEaseOut() {
  const elapsed = performance.now() - startTime;
  const progress = Math.min(elapsed / duration, 1);
  const easedProgress = easeOut(progress);
  
  const currentPos = startPos + (endPos - startPos) * easedProgress;
  element.style.transform = `translateX(${currentPos}px)`;
  
  if (progress < 1) {
    requestAnimationFrame(animateEaseOut);
  }
}
requestAnimationFrame(animateEaseOut);

这里 easeOut(t) 接收一个 0 到 1 的进度值,返回一个也是 0 到 1 的缓动值。区别在于:t 是线性增长的,但 easeOut(t) 的增长速度会越来越慢。

常用的缓动函数就那么几个,我整理一下:

名称 公式 效果
linear t 匀速
easeIn 慢-快
easeOut 1 - (1-t)³ 快-慢
easeInOut t<0.5 ? 4t³ : 1-(1-t)³ 慢-快-慢
核心思路: 缓动函数就是「输入线性时间,输出非线性位置」。你只需要记住这个公式,就能做出各种自然的动画效果。

我个人习惯把缓动函数封装成一个工具库,这样在多个项目里复用。比如在 React Native 里,我会用 Animated.timing 配合 easing 模块;在 Web 里,我会自己写一套类似的接口。

嗯,到这里 JavaScript 动画基础就讲完了。下一章我们会把这些知识应用到实际项目中,看看怎么在 Web、React Native 和 Flutter 里写出高性能的动画。