前端攻城狮们,有没有遇到过这样的尴尬:精心设计的动画效果,一上线就卡成PPT,用户反馈“你们的页面怎么一抖一抖的”?
别急着甩锅给浏览器或者用户电脑配置差,问题可能出在你用的定时器上!今天我们就来聊聊为什么requestAnimationFrame才是做动画的“天选之子”,而setTimeout可能正在默默坑你。
为什么不用setTimeout做动画?
先来说个真实案例。去年我有个朋友接了个电商网站项目,首页有个商品轮播图动画,他用setTimeout实现了,测试时候好好的,上线后却收到一堆投诉说“滑动卡顿”。
他一开始以为是图片太大,压缩了所有图片后问题依旧。最后才发现罪魁祸首竟然是setTimeout!
setTimeout做动画有两个致命伤:
第一,它不关心浏览器的刷新周期。大多数显示器的刷新率是60Hz,也就是每16.7ms刷新一次。但setTimeout只能设置一个固定时间间隔,比如16ms,这就很容易和屏幕刷新节奏错开。
想象一下,你在跳舞,音乐节奏是固定的,但你的舞步总是慢半拍或者快半拍,能不别扭吗?
第二,当页面被隐藏或最小化时,setTimeout还在后台傻傻地执行,白白消耗CPU和电量。用户手机发烫了,还以为是在挖矿呢!
requestAnimationFrame为什么是王道?
requestAnimationFrame(后面我们就叫它rAF吧)是浏览器专门为动画提供的API,它解决了setTimeout的所有痛点。
rAF最大的优势就是:它会在浏览器下一次重绘之前执行回调函数,完美契合显示器的刷新节奏。
还是用跳舞的比喻,这次rAF会先听音乐的节奏,然后精准地踩在每一个拍子上,视觉效果自然流畅多了。
而且当页面不可见时,rAF会自动暂停执行,不会浪费任何资源,这才是真正的“智能省电模式”啊!
手把手教你用rAF实现动画循环
理论知识说够了,来点实际的!下面是一个最简单的rAF动画示例:
function animate() {
// 这里是你的动画逻辑
element.style.left = (parseInt(element.style.left) + 1) + 'px';
// 循环调用
requestAnimationFrame(animate);
}
// 启动动画
requestAnimationFrame(animate);
看到没?就是这么简单!但实际项目中我们通常需要控制动画的开始和结束:
let animationId;
let startTime;
function animate(timestamp) {
if (!startTime) startTime = timestamp;
// 计算动画进度
const progress = timestamp - startTime;
// 执行动画,例如移动100px,用时1000ms
element.style.transform = `translateX(${Math.min(progress / 1000 * 100, 100)}px)`;
if (progress < 1000) {
animationId = requestAnimationFrame(animate);
}
}
// 开始动画
function startAnimation() {
animationId = requestAnimationFrame(animate);
}
// 停止动画
function stopAnimation() {
cancelAnimationFrame(animationId);
}
这里用了timestamp参数,它是rAF回调函数自动传入的时间戳,精度高达微秒级别,比Date.now()精准多了!
性能优化小技巧
虽然rAF已经很优秀了,但用好它还需要一些技巧:
1. 避免在rAF中做耗时操作
rAF的执行频率很高,如果在这里面做大量计算或者DOM操作,还是会卡。复杂计算最好放在Web Worker中。
2. 批量DOM操作
频繁修改DOM样式会触发重排和重绘,最好先计算好所有值,一次性应用。
function animate() {
// 先计算...
const left = computeLeft();
const top = computeTop();
// ...再一次性应用
element.style.cssText = `left: ${left}px; top: ${top}px;`;
requestAnimationFrame(animate);
}
3. 使用transform和opacity
这两个属性不会触发重排,只触发重绘,性能开销小得多。能用transform就别用left/top。
实战案例:平滑滚动到顶部
现在我们来个实战案例,用rAF实现一个平滑滚动到页面顶部的功能:
function scrollToTop() {
const duration = 1000; // 滚动时间
const start = window.pageYOffset;
const startTime = performance.now();
function animate(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 使用easeOut缓动函数让滚动更自然
const easeOut = progress => progress * (2 - progress);
window.scrollTo(0, start * (1 - easeOut(progress)));
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
这个实现比直接使用scrollTo({behavior: ‘smooth’})的兼容性更好,而且可以自定义滚动时长和缓动效果。
兼容性处理
虽然现代浏览器都支持rAF,但如果你还需要支持老古董浏览器,可以加上兼容代码:
window.requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return setTimeout(callback, 1000 / 60);
};
window.cancelAnimationFrame = window.cancelAnimationFrame ||
window.mozCancelAnimationFrame ||
function(id) {
clearTimeout(id);
};
这样即使在不支持rAF的浏览器中,也会回退到setTimeout,虽然效果差些,但至少不会报错。
总结
requestAnimationFrame不是万能的,但在做动画方面,它确实比setTimeout强太多了。就像你不能用螺丝刀去锤钉子一样,选择合适的工具才能事半功倍。