一、什么叫"优化好"的 requestAnimationFrame 动画?
对于常见 60Hz 屏幕,你每一帧可用时间大约 16ms。只要:
- 每个
requestAnimationFrame回调里的 总工作时间 < 16ms(更理想是 10--15ms) - 避免频繁重排(reflow)和重绘(repaint)
- 尽可能把计算和 DOM 更新控制在必要的最小范围内
你的动画在视觉上就会是"平滑的"(60fps 或被浏览器稳定地降到一个一致的帧率,如 30fps)。
二、核心原则
-
用 requestAnimationFrame,而不是
setTimeout/setInterval来驱动动画- requestAnimationFrame 会在浏览器准备重绘前调用回调,自动跟随不同设备刷新率,并在后台标签页暂停,整体更节能、也更平滑[1][3][8]。
-
优先使用 CSS 动画/过渡(transform / opacity)
- 很多简单的位移、缩放、淡入淡出,只用 CSS 就能用 GPU 做"合成层动画",几乎不占 JS 时间。
- 当需要复杂逻辑、交互驱动或时间轴控制时,再用 requestAnimationFrame 做 JS 动画。
-
把 requestAnimationFrame 回调做"瘦身":轻计算 + 少 DOM 操作
- 大块计算逻辑不要放在 requestAnimationFrame 内部,而是拆分、延后或放到 Worker 里。
- requestAnimationFrame 回调内只做:
- 基于当前时间计算下一状态
- 对 DOM 做最小量、批量化的样式更新
三、具体优化策略
1. 使用 GPU 友好的 CSS 属性:transform / opacity
正确做法:
js
// 在 requestAnimationFrame 中
function step(timestamp) {
// 计算新的位移
box.style.transform = `translateX(${x}px)`; // 位移
box.style.opacity = alpha; // 透明度
requestAnimationFrame(step);
}
避免:
js
// 这些属性经常触发布局和重绘
div.style.left = x + 'px';
div.style.top = y + 'px';
div.style.width = w + 'px';
div.style.height = h + 'px';
div.style.margin = m + 'px';
原因:
- transform / opacity 通常只触发合成阶段,可在 GPU 合成层上处理,明显降低主线程压力。
- width / left 等会触发布局(layout)和绘制(paint),容易导致 LoAF(长动画帧 > 50ms)。
2. 统一动画循环:单一 requestAnimationFrame 管理多动画
问题模式(反例):
js
function animateBox1(t) { /* ... */ requestAnimationFrame(animateBox1); }
function animateBox2(t) { /* ... */ requestAnimationFrame(animateBox2); }
function animateBox3(t) { /* ... */ requestAnimationFrame(animateBox3); }
- 多个独立 requestAnimationFrame 回调增加了调度负担,也更难统一限流/暂停。
推荐模式:集中管理 + 统一循环:
js
class AnimationManager {
constructor() {
this.tasks = new Set();
this.animationId = null;
this.fps = 60; // 可调
this.lastFrameTime = performance.now();
}
registerTask(task) {
this.tasks.add(task);
if (this.tasks.size === 1) {
this.animationId = requestAnimationFrame(this.run.bind(this));
}
}
unregisterTask(task) {
this.tasks.delete(task);
if (this.tasks.size === 0 && this.animationId !== null) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
run(currentTime) {
const deltaTime = currentTime - this.lastFrameTime;
const frameInterval = 1000 / this.fps;
if (deltaTime > frameInterval) {
this.tasks.forEach(task => task(currentTime, deltaTime));
this.lastFrameTime = currentTime;
}
this.animationId = requestAnimationFrame(this.run.bind(this));
}
}
使用:
js
const manager = new AnimationManager();
function scaleTask(time) { /* 只做缩放样式更新 */ }
function colorTask(time) { /* 只做颜色样式更新 */ }
function rotateTask(time) { /* 只做旋转样式更新 */ }
manager.registerTask(scaleTask);
manager.registerTask(colorTask);
manager.registerTask(rotateTask);
// 停止某个动画时: manager.unregisterTask(scaleTask);
好处:
- 所有动画共用一条"时脉",便于统一控制 FPS 和暂停/恢复。
- 浏览器只管理一个 requestAnimationFrame 调度点,减少冗余回调开销。
3. 控制 FPS:有意识地"少渲染"
不是所有动画都需要 60fps,例如背景装饰动画完全可以 30fps 或更低。
在上面的 AnimationManager 里,通过修改 this.fps 控制目标帧率:
js
const manager = new AnimationManager();
manager.fps = 30; // 背景动画 30fps,明显降低 CPU 占用
- 多数用户难以分辨 30fps 的轻量动画与 60fps 的差别,但 CPU/GPU 开销可以明显下降(移动设备、电池尤为受益)。
4. 拆分重任务,避免"长动画帧"(LoAF)
LoAF 定义:单帧时长超过约 50ms 即可视作"长动画帧"[5],很容易造成明显卡顿。
不要这样:
js
requestAnimationFrame(() => {
// 这里做了一个 50ms 的大循环运算
for (let i = 0; i < 1_000_000; i++) { /* ...重运算... */ }
// 然后再更新 DOM
});
拆成小块跨帧执行:
js
function heavyTaskChunk(start, done) {
const CHUNK_SIZE = 1000;
for (let i = start; i < 1_000_000 && i < start + CHUNK_SIZE; i++) {
// 批量处理数据
}
if (start + CHUNK_SIZE < 1_000_000) {
// 把剩余任务交给后续帧 / 空闲时间继续跑
setTimeout(() => heavyTaskChunk(start + CHUNK_SIZE, done), 0);
} else if (done) {
done();
}
}
// 在动画外部安排重任务
setTimeout(() => heavyTaskChunk(0, () => console.log('完成')), 0);
要点:
- requestAnimationFrame 回调只负责动画状态与 DOM 渲染 ;重计算要么拆分到
setTimeout/requestIdleCallback,要么放到 Web Worker。 - 对交互来说,更重要的是让主线程"经常有机会空转",以便随时处理用户输入。
5. 避免 Layout Thrashing:先读后写
错误模式:
js
// 每次循环既读又写,会不断强制浏览器重新布局
for (const el of elements) {
el.style.width = el.offsetWidth + '10px'; // 读 offsetWidth 立刻触发布局
}
正确模式:批量读 / 批量写:
js
const widths = [];
// 1. 先把所有读操作做完
for (let i = 0; i < elements.length; i++) {
widths[i] = elements[i].offsetWidth;
}
// 2. 再统一写
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] + 10 + 'px';
}
在 requestAnimationFrame 里尤其要遵守这个模式:
- 先在一段逻辑中读所有需要的布局信息 (如
offsetWidth、scrollTop等) - 然后一段逻辑中统一更新样式(写 style)
6. 观察者回调中不要做重工作
像 ResizeObserver、IntersectionObserver 的回调本身就是在接近绘制阶段调用的,如果在里面做重计算,会直接"吃掉这一帧的预算"。
改进:
js
const resizeObserver = new ResizeObserver(entries => {
// 推迟到下一轮 event loop / 下一帧处理
setTimeout(() => {
dealWithResize(entries);
}, 0);
});
7. 善用 CSS 动画,减少 JS 参与
- 尽量让"可预测、无需交互控制"的动画使用 CSS(
@keyframes或transition)。 - 某些浏览器在 JS 繁忙时,仍可让 CSS 动画在合成线程上相对平滑地继续运行,这是 JS requestAnimationFrame 做不到的。
例如:
css
.box {
animation: float 2s ease-in-out infinite alternate;
}
@keyframes float {
from { transform: translateY(0); }
to { transform: translateY(-10px); }
}
此时 JS 完全不参与动画过程,只负责业务逻辑。
8. 初级性能排查流程(实战向)
-
用 Chrome DevTools Performance 录制动画场景
- 检查
Frames时间条,是否大量帧超出 16ms,尤其 > 50ms。
- 检查
-
找出最重的 requestAnimationFrame 回调 / 事件处理
- 重点看:
- JavaScript 运行时间(JS flame chart)
- layout / paint 时间是否过长
- 有无紧接着的强制布局操作(读写交错)
- 重点看:
-
按优先级优化
- 把影响最大的 JS 计算拆分或移出 requestAnimationFrame(见第 4 条)。
- 把所有动画相关样式改成
transform/opacity(见第 1 条)。 - 合并 requestAnimationFrame 调用,使用统一动画管理器,并按需限 FPS(见第 2、3 条)。
- 检查并修正 layout thrashing(见第 5 条)。
- 确认观察者和输入事件中没有重任务(见第 6 条)。
九、简要实践清单
编写 requestAnimationFrame 动画时,请保证:
- 动画驱动全部使用
requestAnimationFrame,不用setInterval做高频更新 - requestAnimationFrame 回调内只做:轻量状态计算 + transform/opacity 更新
- 不在 requestAnimationFrame 回调中执行大循环或复杂算法
- 如果有多个动画,尽量通过统一的 AnimationManager 来集中调度
- 非关键动画(装饰类)使用较低 FPS(如 30fps)
- 所有布局读写分离(先读后写),避免反复触发布局
- 尽量让"可预测"的动画交给 CSS transitions/animations 完成
- 通过 Performance/DevTools 确认没有明显长帧(> 50ms)
References
1\] CSS and JavaScript animation performance. [developer.mozilla.org/en-US/docs/...](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FPerformance%2FGuides%2FCSS_JavaScript_animation_performance "https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/CSS_JavaScript_animation_performance") \[2\] Optimize requestAnimationFrame Like a Pro. [dev.to/josephciull...](https://link.juejin.cn?target=https%3A%2F%2Fdev.to%2Fjosephciullo%2Fsupercharge-your-web-animations-optimize-requestanimationframe-like-a-pro-22i5 "https://dev.to/josephciullo/supercharge-your-web-animations-optimize-requestanimationframe-like-a-pro-22i5") \[3\] Jank busting for better rendering performance. [web.dev/articles/sp...](https://link.juejin.cn?target=https%3A%2F%2Fweb.dev%2Farticles%2Fspeed-rendering "https://web.dev/articles/speed-rendering") \[5\] Fixing Long Animation Frames (LoAF). [requestmetrics.com/web-perform...](https://link.juejin.cn?target=https%3A%2F%2Frequestmetrics.com%2Fweb-performance%2Ffixing-long-animation-frame-loaf%2F "https://requestmetrics.com/web-performance/fixing-long-animation-frame-loaf/")