requestAnimationFrame
window.requestAnimationFrame() 方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次**重绘之前**,调用用户提供的回调函数
。 相比于使用 setTimeout 或 setInterval,requestAnimationFrame 更加流畅且节能,因为它会:
- 自动跟随显示器刷新率(通常是 60FPS,每帧约 16.67ms)
- 在页面不活动(如切换标签页)时自动暂停,节省资源
- 更好地避免掉帧或卡顿现象
✅ 基本语法
function update() {
// 执行动画更新逻辑,比如移动一个元素
console.log('每帧更新');
requestAnimationFrame(update); // 递归调用
}
requestAnimationFrame(update);
返回值
请求 ID 是一个 long 类型整数值,是在回调列表里的唯一标识符。这是一个非零值,但你不能对该值做任何其他假设。你可以将此值传递给 window.cancelAnimationFrame() 函数以取消该刷新回调请求
。
WARNING
若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 requestAnimationFrame()。requestAnimationFrame() 是一次性的
实例
一个元素的动画时间是 2 秒(2000 毫秒)。该元素以 0.1px/ms 的速度向右移动,所以它的相对位置(以 CSS 像素为单位)可以通过动画开始后所经过的时间(以毫秒为单位)的函数来计算,即 0.1 * elapsed。该元素的最终位置是在其初始位置的右边 200px(0.1 * 2000)。
const element = document.getElementById("some-element-you-want-to-animate");
let start, previousTimeStamp;
let done = false;
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimeStamp !== timestamp) {
// 这里使用 Math.min() 确保元素在恰好位于 200px 时停止运动
const count = Math.min(0.1 * elapsed, 200);
element.style.transform = `translateX(${count}px)`;
if (count === 200) done = true;
}
if (elapsed < 2000) {
// 2 秒之后停止动画
previousTimeStamp = timestamp;
if (!done) {
window.requestAnimationFrame(step);
}
}
}
window.requestAnimationFrame(step);
实现一个进度条
<head>
<title>js实现一个不卡顿的进度条</title>
</head>
<style>
.box {
margin-bottom: 100px;
}
</style>
<div class="box">
<div id="progress" style="background-color: lightblue; width: 0; height: 20px; line-height: 20px">
0%
</div>
<div>requestAnimationFrame <button id="btn">run</button></div>
</div>
<script>
let timer
let i = 0,
max = 200000
let fnTimer = 0
let btn = document.getElementById('btn')
let progress = document.getElementById('progress')
btn.onclick = function () {
progress.style.width = '0'
i = 0
const step = () => {
const width = Number.parseInt(progress.style.width)
if (width < 500) {
progress.style.width = width + 5 + 'px'
progress.innerText = width / 5 + 1 + '%'
console.log('progress.innerHTML', progress.innerHTML)
timer = requestAnimationFrame(step)
} else {
cancelAnimationFrame(step)
}
}
requestAnimationFrame(step)
requestIdleCallback(nonEssentialWork)
}
function add() {
i++
console.log(i)
}
function nonEssentialWork(idleDeadline) {
console.log('fnIndex', ++fnTimer, idleDeadline.timeRemaining())
while (idleDeadline.timeRemaining() > 0 && i < max) {
add()
}
if (i < max) {
requestIdleCallback(nonEssentialWork)
}
}
</script>
settimeout
jsbtn3.onclick = function () { clearTimeout(timer) progress.style.width = '0' timer = setTimeout(function fn() { if (parseInt(progress.style.width) < 500) { progress.style.width = parseInt(progress.style.width) + 5 + 'px' progress.innerHTML = parseInt(progress.style.width) / 5 + '%' timer = setTimeout(fn, 16) } else { clearTimeout(timer) } }, 0) }
setInterval
jsbtn2.onclick = function () { clearInterval(timer) progress.style.width = '0' timer = setInterval(function () { if (parseInt(progress.style.width) < 500) { progress.style.width = parseInt(progress.style.width) + 5 + 'px' progress.innerHTML = parseInt(progress.style.width) / 5 + '%' } else { clearInterval(timer) } }, 0) }
requestAnimationFrame 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
jsbtn1.onclick = function () { progress.style.width = '0' const step = () => { const width = parseInt(progress.style.width) if (width < 500) { progress.style.width = width + 5 + 'px' progress.innerHTML = width / 5 + '%' console.log('progress.innerHTML', progress.innerHTML) timer = requestAnimationFrame(step) } else { cancelAnimationFrame(timer) } } timer = requestAnimationFrame(step) }
图片懒加载 + 缓存预热
使用 requestAnimationFrame 和 requestIdleCallback
🎯 场景目标
页面滚动时,用户看到图片就立刻加载(使用 IntersectionObserver + requestAnimationFrame),但在浏览器空闲时间再去预加载后续图片(使用 requestIdleCallback),提升性能体验。
<!-- 模拟图片列表 -->
<img data-src="img1.jpg" alt="" />
<img data-src="img2.jpg" alt="" />
<img data-src="img3.jpg" alt="" />
<img data-src="img4.jpg" alt="" />
<img data-src="img5.jpg" alt="" />
// 获取所有懒加载图片
const lazyImages = document.querySelectorAll('img[data-src]');
const preloadQueue = [];
// 创建 Intersection Observer
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 用 requestAnimationFrame 确保在绘制前执行
requestAnimationFrame(() => {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
obs.unobserve(img);
console.log('懒加载图片:', img.src);
});
} else {
// 未进入视口的图片放入预加载队列
preloadQueue.push(entry.target);
}
});
}, {
rootMargin: '100px', // 提前一点加载
threshold: 0.1
});
// 开始观察所有图片
lazyImages.forEach(img => observer.observe(img));
// 浏览器空闲时预加载剩余图片
function preloadIdleImages(deadline) {
while (deadline.timeRemaining() > 0 && preloadQueue.length > 0) {
const img = preloadQueue.shift();
if (img && img.dataset.src) {
const tempImg = new Image();
tempImg.src = img.dataset.src;
console.log('后台预热缓存:', tempImg.src);
}
}
// 如果还有任务,继续排队
if (preloadQueue.length > 0) {
requestIdleCallback(preloadIdleImages);
}
}
// 启动后台预加载
if ('requestIdleCallback' in window) {
requestIdleCallback(preloadIdleImages);
} else {
// fallback: 延迟执行
setTimeout(() => preloadIdleImages({ timeRemaining: () => 50 }), 200);
}
🔍 工作流程说明
- 用户滚动到图片时,用 IntersectionObserver 检测它进入视口。
- 用 requestAnimationFrame 确保图片加载逻辑在下一帧渲染前执行(平滑)。
- 其它没进入视口的图片,在浏览器空闲时用 requestIdleCallback 执行预加载。
- 提前缓存图片数据,加快用户下一次看到时的加载速度。