requestAnimationFrame 详解
你是否遇到过这样的问题:用 setInterval 做的动画在页面卡顿时变得忽快忽慢?或者滚动事件中频繁操作 DOM 导致页面掉帧?requestAnimationFrame(简称 rAF)正是浏览器提供的高性能动画与渲染调度 API,它能让你的代码与浏览器的刷新节奏完美同步,实现丝滑的 60fps 动画效果。
1. 什么是 requestAnimationFrame?
requestAnimationFrame 是浏览器提供的一个 API,用于在下一次重绘(repaint)之前执行指定的回调函数。它的核心思想是:让 JavaScript 的执行时机与浏览器的渲染周期对齐。
requestAnimationFrame 让你的代码跟着浏览器的节奏走,不多不少,每一帧恰好执行一次,避免了无效计算和丢帧问题。
2. 基本语法
2.1 请求动画帧
const id = requestAnimationFrame(callback);
- callback:在下一次重绘前调用的函数,接收一个
DOMHighResTimeStamp参数(高精度时间戳) - 返回值:一个整数 ID,用于取消该请求
2.2 取消动画帧
cancelAnimationFrame(id);
2.3 最简示例
function animate(timestamp) {
// timestamp 是从页面加载开始的高精度时间(毫秒)
console.log('当前时间戳:', timestamp);
// 递归调用,形成动画循环
requestAnimationFrame(animate);
}
// 启动动画
requestAnimationFrame(animate);
回调函数接收的 timestamp 是一个 DOMHighResTimeStamp,精度可达微秒级别。它表示从 performance.timeOrigin(通常是页面导航开始时)到当前帧的时间,与 performance.now() 同源。
3. 浏览器渲染机制与 rAF 的关系
要理解 requestAnimationFrame 为什么好用,我们需要先了解浏览器的一帧里发生了什么。
3.1 一帧的生命周期
大多数显示器刷新率为 60Hz,即每秒刷新 60 次,一帧约 16.67ms。浏览器需要在这个时间窗口内完成所有工作:
3.2 rAF 的执行时机
从上面的流程可以看出,requestAnimationFrame 的回调在以下时刻执行:
- 在 JavaScript 主任务之后 — 不会被其他脚本抢占
- 在样式/布局计算之前 — 你对 DOM 的修改能在当前帧中生效
- 在绘制之前 — 确保用户看到的是最新的状态
requestAnimationFrame 的回调不属于宏任务,也不属于微任务,它是浏览器渲染流水线中一个独立的阶段。不要将它与 setTimeout(fn, 0) 混淆。
4. 为什么不用 setInterval / setTimeout?
4.1 对比分析
| 对比维度 | setTimeout / setInterval | requestAnimationFrame |
|---|---|---|
| 执行时机 | 与渲染周期无关,可能在帧的任何时刻 | 精确在下一次渲染前 |
| 帧率同步 | 无法自动同步显示器刷新率 | 自动匹配刷新率(60Hz/120Hz/144Hz) |
| 后台行为 | 继续执行,浪费 CPU | 自动暂停,节省资源 |
| 时间精度 | 受最小延迟限制(嵌套 ≥ 4ms) | 高精度时间戳 |
| 丢帧问题 | 可能在一帧内多次执行或跳帧 | 每帧最多执行一次 |
| 电池影响 | 持续消耗电量 | 后台自动停止,省电 |
4.2 定时器的问题演示
// ❌ 使用 setInterval 的动画 — 有很多问题
let position = 0;
setInterval(() => {
position += 2;
element.style.transform = `translateX(${position}px)`;
}, 16); // 试图模拟 60fps,但实际并不精确
// 问题 1: setInterval 的最小间隔并非精确的 16ms
// 问题 2: 标签页切到后台时仍然执行,浪费资源
// 问题 3: 如果回调执行时间 > 16ms,会导致任务堆积
// ✅ 使用 requestAnimationFrame — 完美方案
let position = 0;
function move() {
position += 2;
element.style.transform = `translateX(${position}px)`;
if (position < 300) {
requestAnimationFrame(move);
}
}
requestAnimationFrame(move);
// 优势 1: 精确同步浏览器刷新率
// 优势 2: 标签页不可见时自动暂停
// 优势 3: 每帧只执行一次,不会堆积
4.3 高刷新率屏幕下的差异
现代设备的屏幕刷新率越来越高(120Hz、144Hz),requestAnimationFrame 能自动适配,而定时器只能以固定频率运行。
5. 核心用法与实战示例
5.1 基于时间的动画(推荐)
直接根据位移量递增会导致动画速度受帧率影响。正确做法是**基于时间差(deltaTime)**来计算动画进度:
function createAnimation(element, duration, distance) {
let startTime = null;
function animate(timestamp) {
// 记录动画开始时间
if (!startTime) startTime = timestamp;
// 计算经过的时间
const elapsed = timestamp - startTime;
// 计算进度(0 ~ 1)
const progress = Math.min(elapsed / duration, 1);
// 应用动画效果
element.style.transform = `translateX(${progress * distance}px)`;
// 动画未结束则继续
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// 使用:让元素在 1 秒内向右移动 300px
const box = document.querySelector('.box');
createAnimation(box, 1000, 300);
无论是 60Hz 还是 144Hz 的屏幕,动画总时长都是一样的。在 60Hz 屏幕上每帧移动更多,在 144Hz 屏幕上每帧移动更少,但最终效果一致。这就是**帧率无关(frame-rate independent)**动画。
5.2 添加缓动函数
线性动画看起来很机械,通过缓动函数可以让动画更自然:
// 常用缓动函数
const easings = {
// 线性
linear: (t) => t,
// 缓入(慢 → 快)
easeIn: (t) => t * t,
// 缓出(快 → 慢)
easeOut: (t) => t * (2 - t),
// 缓入缓出
easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
// 弹性效果
easeOutBounce: (t) => {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) return n1 * t * t;
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
return n1 * (t -= 2.625 / d1) * t + 0.984375;
},
};
function animateWithEasing(element, duration, distance, easing = 'easeOut') {
let startTime = null;
const easingFn = easings[easing];
function step(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const linearProgress = Math.min(elapsed / duration, 1);
// 将线性进度传入缓动函数,得到缓动后的进度
const easedProgress = easingFn(linearProgress);
element.style.transform = `translateX(${easedProgress * distance}px)`;
if (linearProgress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
// 使用缓出效果
animateWithEasing(box, 800, 300, 'easeOut');
5.3 可控制的动画对象
在实际项目中,我们经常需要暂停、恢复、取消动画:
function createControllableAnimation(element, options) {
const { duration, from, to, easing = 'linear', onUpdate, onComplete } = options;
let startTime = null;
let pausedTime = null;
let rafId = null;
let totalPausedDuration = 0;
const easingFn = easings[easing] || easings.linear;
function step(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime - totalPausedDuration;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easingFn(progress);
const currentValue = from + (to - from) * easedProgress;
if (onUpdate) {
onUpdate(currentValue, easedProgress);
}
if (progress < 1) {
rafId = requestAnimationFrame(step);
} else if (onComplete) {
onComplete();
}
}
// 返回控制对象
return {
// 开始动画
start() {
startTime = null;
totalPausedDuration = 0;
rafId = requestAnimationFrame(step);
return this;
},
// 暂停动画
pause() {
if (rafId) {
cancelAnimationFrame(rafId);
pausedTime = performance.now();
rafId = null;
}
return this;
},
// 恢复动画
resume() {
if (!rafId && pausedTime) {
totalPausedDuration += performance.now() - pausedTime;
pausedTime = null;
rafId = requestAnimationFrame(step);
}
return this;
},
// 取消动画
cancel() {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
return this;
},
};
}
// 使用示例
const anim = createControllableAnimation(box, {
duration: 2000,
from: 0,
to: 500,
easing: 'easeInOut',
onUpdate(value) {
box.style.transform = `translateX(${value}px)`;
},
onComplete() {
console.log('动画完成!');
},
});
anim.start();
// 1 秒后暂停
setTimeout(() => anim.pause(), 1000);
// 2 秒后恢复
setTimeout(() => anim.resume(), 2000);
5.4 滚动事件节流
requestAnimationFrame 的一个高频应用场景是事件节流,避免在一帧内多次触发 DOM 操作:
// ❌ 不好的做法:每次滚动事件都操作 DOM
window.addEventListener('scroll', () => {
// 滚动事件每秒可能触发几十上百次
header.style.transform = `translateY(${window.scrollY}px)`;
updateParallax();
checkLazyLoad();
});
// ✅ 使用 rAF 节流:每帧只执行一次
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
header.style.transform = `translateY(${window.scrollY}px)`;
updateParallax();
checkLazyLoad();
ticking = false;
});
ticking = true;
}
});
封装成通用的节流函数:
function rafThrottle(callback) {
let ticking = false;
return function (...args) {
if (!ticking) {
requestAnimationFrame(() => {
callback.apply(this, args);
ticking = false;
});
ticking = true;
}
};
}
// 使用
window.addEventListener('scroll', rafThrottle((e) => {
console.log('每帧只执行一次', window.scrollY);
}));
window.addEventListener('resize', rafThrottle(() => {
recalculateLayout();
}));
window.addEventListener('mousemove', rafThrottle((e) => {
tooltip.style.left = e.clientX + 'px';
tooltip.style.top = e.clientY + 'px';
}));
5.5 批量 DOM 操作优化
在需要一次性更新大量 DOM 元素时,requestAnimationFrame 可以将操作集中到一帧内执行:
// ❌ 不好的做法:散落在各处的 DOM 操作可能导致多次重排
function updateItems(items) {
items.forEach((item) => {
item.element.style.width = item.width + 'px'; // 触发重排
item.element.style.height = item.height + 'px'; // 再次重排
item.element.style.transform = `translate(${item.x}px, ${item.y}px)`;
});
}
// ✅ 好的做法:读写分离 + rAF 集中写入
function updateItems(items) {
// 第一步:集中读取(不会触发重排)
const measurements = items.map((item) => ({
rect: item.element.getBoundingClientRect(),
data: item,
}));
// 第二步:在下一帧集中写入
requestAnimationFrame(() => {
measurements.forEach(({ data }) => {
data.element.style.width = data.width + 'px';
data.element.style.height = data.height + 'px';
data.element.style.transform = `translate(${data.x}px, ${data.y}px)`;
});
});
}
6. 高级技巧
6.1 帧率检测器(FPS Monitor)
利用 requestAnimationFrame 可以实时检测页面帧率:
function createFPSMonitor() {
let frameCount = 0;
let lastTimestamp = performance.now();
let fps = 0;
function tick(timestamp) {
frameCount++;
// 每秒统计一次
if (timestamp - lastTimestamp >= 1000) {
fps = Math.round((frameCount * 1000) / (timestamp - lastTimestamp));
frameCount = 0;
lastTimestamp = timestamp;
console.log(`当前 FPS: ${fps}`);
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
return {
getFPS: () => fps,
};
}
const monitor = createFPSMonitor();
6.2 基于 rAF 的倒计时器
function createCountdown(seconds, onTick, onComplete) {
const endTime = performance.now() + seconds * 1000;
function tick(timestamp) {
const remaining = Math.max(0, endTime - timestamp);
const remainingSeconds = Math.ceil(remaining / 1000);
onTick(remainingSeconds);
if (remaining > 0) {
requestAnimationFrame(tick);
} else {
onComplete();
}
}
requestAnimationFrame(tick);
}
// 使用:10 秒倒计时
createCountdown(
10,
(seconds) => {
display.textContent = `${seconds} 秒`;
},
() => {
display.textContent = '时间到!';
}
);
6.3 Canvas 动画循环
requestAnimationFrame 是 Canvas 动画的标配:
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 粒子系统示例
const particles = [];
function createParticle() {
return {
x: Math.random() * canvas.width,
y: canvas.height,
vx: (Math.random() - 0.5) * 2,
vy: -Math.random() * 3 - 1,
size: Math.random() * 4 + 1,
life: 1, // 1 = 刚创建,0 = 消失
};
}
let lastTime = 0;
function gameLoop(timestamp) {
// 计算时间差(秒)
const deltaTime = (timestamp - lastTime) / 1000;
lastTime = timestamp;
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 每帧添加新粒子
if (particles.length < 200) {
particles.push(createParticle());
}
// 更新并绘制粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
// 更新位置
p.x += p.vx * deltaTime * 60;
p.y += p.vy * deltaTime * 60;
p.life -= deltaTime * 0.5;
// 移除死亡粒子
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
// 绘制
ctx.globalAlpha = p.life;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = '#4fc3f7';
ctx.fill();
}
ctx.globalAlpha = 1;
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
6.4 与 CSS 动画配合
有些场景下需要在 JavaScript 修改样式后等待浏览器重新计算,可以利用双 rAF 技巧:
// 双 rAF 技巧:确保浏览器完成一次渲染后再修改
function afterNextPaint(callback) {
requestAnimationFrame(() => {
requestAnimationFrame(callback);
});
}
// 场景:先设置初始状态,再触发 CSS 过渡动画
function showElement(el) {
// 第一步:设置初始状态(浏览器还没渲染)
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.3s, transform 0.3s';
el.style.display = 'block';
// 第二步:等浏览器渲染初始状态后,再切换到最终状态
afterNextPaint(() => {
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
});
}
单个 requestAnimationFrame 的回调可能在浏览器计算样式之前执行,如果你在同一个 rAF 中先设置 opacity: 0 再设置 opacity: 1,浏览器可能会将两次修改合并,导致过渡动画不触发。双 rAF 确保第一次修改已经渲染到屏幕后,第二次修改才开始。
7. 常见陷阱与注意事项
7.1 回调只执行一次
// ❌ 错误理解:以为会持续调用
requestAnimationFrame(() => {
element.style.left = parseInt(element.style.left) + 1 + 'px';
// 只会执行一次!不会自动循环!
});
// ✅ 正确做法:手动递归调用
function animate() {
element.style.left = parseInt(element.style.left) + 1 + 'px';
requestAnimationFrame(animate); // 需要手动递归
}
requestAnimationFrame(animate);
7.2 标签页不可见时暂停
当用户切换到其他标签页时,requestAnimationFrame 的回调会停止执行(大多数浏览器会降低到每秒 0~1 次)。这是一个优点,但也需要注意:
// ❌ 问题:切回标签页后动画可能"跳跃"
let lastTime = 0;
function animate(timestamp) {
const delta = timestamp - lastTime;
lastTime = timestamp;
// 如果标签页不可见 5 秒后切回,delta 会是 5000ms
// 角色可能一下子移动了很远!
player.x += speed * delta;
requestAnimationFrame(animate);
}
// ✅ 解决方案:限制最大 delta 值
function animate(timestamp) {
const delta = timestamp - lastTime;
lastTime = timestamp;
// 限制最大时间差,防止标签页切换导致的跳跃
const clampedDelta = Math.min(delta, 100); // 最多当作 100ms
player.x += speed * clampedDelta;
requestAnimationFrame(animate);
}
7.3 内存泄漏
如果不在适当的时候停止 rAF,可能造成内存泄漏:
// ❌ 组件卸载后仍在执行
class AnimatedComponent {
start() {
const animate = () => {
this.update();
requestAnimationFrame(animate); // 永远不会停止
};
requestAnimationFrame(animate);
}
}
// ✅ 在组件销毁时取消
class AnimatedComponent {
constructor() {
this.rafId = null;
}
start() {
const animate = () => {
this.update();
this.rafId = requestAnimationFrame(animate);
};
this.rafId = requestAnimationFrame(animate);
}
destroy() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
}
}
在 React 中的正确用法:
import { useEffect, useRef } from 'react';
function AnimatedComponent() {
const rafIdRef = useRef(null);
useEffect(() => {
function animate(timestamp) {
// 动画逻辑...
rafIdRef.current = requestAnimationFrame(animate);
}
rafIdRef.current = requestAnimationFrame(animate);
// 清理:组件卸载时取消
return () => {
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
};
}, []);
return <canvas id="myCanvas" />;
}
7.4 避免在 rAF 回调中执行耗时操作
// ❌ 危险:耗时操作会阻塞渲染
requestAnimationFrame(() => {
// 如果这个操作耗时超过 16ms,就会导致丢帧
heavyComputation();
updateDOM();
});
// ✅ 将耗时计算拆分到多帧中执行
function processInChunks(data, chunkSize, processFn, onComplete) {
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, data.length);
for (let i = index; i < end; i++) {
processFn(data[i], i);
}
index = end;
if (index < data.length) {
requestAnimationFrame(processChunk);
} else {
onComplete();
}
}
requestAnimationFrame(processChunk);
}
// 将 10000 条数据分成每帧处理 100 条
processInChunks(
largeArray,
100,
(item) => renderItem(item),
() => console.log('全部处理完成')
);
8. rAF 与其他调度 API 的对比
| API | 执行时机 | 适用场景 | 帧率感知 | 后台行为 |
|---|---|---|---|---|
setTimeout | 至少延迟 N 毫秒后 | 延时逻辑、去抖 | 否 | 继续执行(可能被限流) |
setInterval | 每隔 N 毫秒 | 定时轮询 | 否 | 继续执行(可能被限流) |
requestAnimationFrame | 下次渲染前 | 动画、DOM 操作 | 是 | 暂停 |
requestIdleCallback | 浏览器空闲时 | 非紧急计算、预加载 | 否 | 暂停 |
queueMicrotask | 当前任务末尾 | Promise 后续、状态同步 | 否 | 与宏任务一起执行 |
9. 浏览器兼容性
requestAnimationFrame 已被所有现代浏览器广泛支持:
| 浏览器 | 支持版本 |
|---|---|
| Chrome | 24+ |
| Firefox | 23+ |
| Safari | 6.1+ |
| Edge | 12+ |
| IE | 10+ |
| iOS Safari | 7+ |
| Android Browser | 4.4+ |
如果需要兼容极旧浏览器,可以使用 polyfill:
// 简易 polyfill(了解原理即可,现代项目基本不需要)
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function (callback) {
return setTimeout(callback, 1000 / 60);
};
window.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
}
10. 面试高频问答
Q1: requestAnimationFrame 是宏任务还是微任务?
都不是。 requestAnimationFrame 回调既不属于宏任务队列,也不属于微任务队列。它属于浏览器渲染流水线中的一个独立步骤,在微任务清空之后、浏览器执行样式计算和布局之前执行。
执行顺序:宏任务 → 微任务(全部清空)→ rAF 回调 → 渲染
console.log('1 - 同步代码');
setTimeout(() => console.log('2 - setTimeout(宏任务)'), 0);
Promise.resolve().then(() => console.log('3 - Promise(微任务)'));
requestAnimationFrame(() => console.log('4 - rAF'));
// 输出顺序:1 → 3 → 4 → 2
// 注意:rAF 和 setTimeout 的顺序取决于浏览器的具体调度,
// 但 rAF 通常在同一帧内先于下一轮宏任务执行
Q2: requestAnimationFrame 和 setTimeout(fn, 0) 有什么区别?
| 区别点 | requestAnimationFrame | setTimeout(fn, 0) |
|---|---|---|
| 执行时机 | 下一次渲染前 | 下一轮事件循环(最少约 4ms 延迟) |
| 调用频率 | 与显示器刷新率一致 | 不受刷新率限制 |
| 后台标签页 | 暂停执行 | 继续执行(可能被限流到 1 次/秒) |
| 时间参数 | 高精度 timestamp | 无 |
| 主要用途 | 动画、视觉更新 | 异步任务调度 |
Q3: 为什么 requestAnimationFrame 比 setInterval 更适合做动画?
- 帧率同步:rAF 自动与显示器刷新率对齐,不需要手动猜测间隔时间
- 不会丢帧:每帧最多执行一次回调,不会出现定时器堆积问题
- 自动省电:标签页不可见时自动暂停,而
setInterval会持续消耗 CPU - 高精度时间戳:回调接收精确的时间戳参数,方便做基于时间的动画
- 浏览器优化:浏览器可以将多个 rAF 回调合并优化,提升渲染性能
Q4: 什么是双 rAF 技巧(Double rAF)?
双 rAF 是指嵌套两层 requestAnimationFrame:
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 在这里执行操作
});
});
用途: 确保浏览器至少完成了一次完整的渲染后再执行操作。常见场景是先设置 CSS 初始状态,等浏览器渲染后再修改为目标状态,从而触发 CSS 过渡动画。
第一个 rAF 的回调在当前帧渲染前执行,第二个 rAF 的回调在下一帧渲染前执行,这中间浏览器一定完成了一次完整的渲染。
Q5: requestAnimationFrame 在高刷新率(120Hz/144Hz)屏幕上表现如何?
requestAnimationFrame 会自动适配显示器的刷新率:
- 60Hz 屏幕:回调约每 16.67ms 调用一次(60 次/秒)
- 120Hz 屏幕:回调约每 8.33ms 调用一次(120 次/秒)
- 144Hz 屏幕:回调约每 6.94ms 调用一次(144 次/秒)
这就是为什么基于时间(deltaTime)做动画比基于固定步长更重要——帧率不同意味着每帧的时间间隔不同。
Q6: 如何用 requestAnimationFrame 实现节流?
function rafThrottle(fn) {
let ticking = false;
return function (...args) {
if (!ticking) {
requestAnimationFrame(() => {
fn.apply(this, args);
ticking = false;
});
ticking = true;
}
};
}
原理:利用布尔标志位 ticking,在一帧内只允许注册一次 rAF 回调。回调执行完毕后重置标志位,允许下一帧的调度。这样保证了每帧最多执行一次回调函数。
Q7: requestAnimationFrame 回调中如何避免内存泄漏?
关键做法:
- 保存 rAF ID,在组件销毁时调用
cancelAnimationFrame(id)取消 - 在 React 中使用
useEffect的清理函数取消 rAF - 在 Vue 中使用
onUnmounted生命周期钩子取消 rAF - 设置终止条件,避免无限递归调用
// Vue 3 示例
import { onMounted, onUnmounted, ref } from 'vue';
const rafId = ref(null);
onMounted(() => {
function animate() {
// 动画逻辑
rafId.value = requestAnimationFrame(animate);
}
rafId.value = requestAnimationFrame(animate);
});
onUnmounted(() => {
if (rafId.value) {
cancelAnimationFrame(rafId.value);
}
});
Q8: requestAnimationFrame 和 CSS 动画/transition 该如何选择?
| 场景 | 推荐方案 |
|---|---|
| 简单的状态过渡(颜色、位置、透明度) | CSS transition |
| 循环播放的关键帧动画 | CSS @keyframes + animation |
| 需要根据用户输入实时计算的动画 | requestAnimationFrame |
| Canvas/WebGL 渲染循环 | requestAnimationFrame |
| 需要精确控制(暂停/恢复/反转)的动画 | requestAnimationFrame 或 Web Animations API |
| 页面滚动联动效果 | requestAnimationFrame + 滚动事件 |
原则: 能用 CSS 实现的优先用 CSS(浏览器可以在 compositor 线程中处理,不阻塞主线程)。需要 JavaScript 逻辑参与计算的,用 requestAnimationFrame。
11. 总结
requestAnimationFrame 是前端性能优化的基石之一。记住它的核心原则:让浏览器决定何时执行你的代码,而不是用定时器去猜测渲染时机。掌握它,你就掌握了流畅动画的钥匙。