requestAnimationFrame 不能直接监控下载进度,仅用于平滑渲染;需结合 Fetch API 和 ReadableStream 获取进度,并用 rAF 插值更新 UI,避免频繁 DOM 操作导致卡顿。

requestAnimationFrame 本身不能直接监控或驱动文件下载进度,它只是浏览器提供的、用于在下一次重绘前执行动画回调的机制。大文件下载是网络 I/O 行为,进度需依赖 Fetch API + ReadableStream + progress event(或手动分块读取) 获取,而 requestAnimationFrame 的作用是——把零散、不规则的进度更新,“插值平滑”地渲染到 UI 上,避免卡顿、跳变或过度重绘。
为什么不能直接用 onprogress 或 fetch().then() 更新进度条?
原生 XMLHttpRequest 的 onprogress 或 fetch 配合 ReadableStream 的 getReader() 可以拿到字节级进度,但这些回调触发频率高、时间间隔不均(尤其在高速下载时可能密集触发,低速时又稀疏),若每次回调都直接操作 DOM(如设置 element.style.width),会导致:
- 短时间内大量强制同步布局/重绘,引发掉帧
- 进度值跳跃(例如从 12% 突然跳到 87%),UI 不连贯
- 主线程阻塞,影响其他交互响应
用 requestAnimationFrame 实现“平滑插值”的核心思路
不把下载进度当作“绝对值”直接渲染,而是把它作为“目标值”,再用 rAF 在每一帧中逐步逼近这个目标。关键在于维护两个状态:当前显示进度(current) 和 最新接收进度(target),并在 rAF 循环中做缓动更新。
- 监听流式下载,持续更新
target = downloadedBytes / totalBytes - 启动一个 rAF 循环,每帧按一定步长(如 0.5%~2%)向 target 靠拢:
current = current + (target - current) * easingFactor - 用
current更新进度条宽度和文案,视觉上就是“缓缓追上”的流畅效果 - 当
Math.abs(target - current) (如 0.001)时停止 rAF
完整可运行示例(Fetch + Stream + rAF 插值)
以下代码适用于现代浏览器(Chrome/Firefox/Edge ≥ 95),支持 GB 级文件,进度条动画丝滑无卡顿:
立即学习“前端免费学习笔记(深入)”;
async function downloadWithSmoothProgress(url, progressBar, statusText) {
const response = await fetch(url);
if (!response.body) throw new Error('ReadableStream not supported');
<p>const contentLength = response.headers.get('content-length');
const total = contentLength ? parseInt(contentLength, 10) : null;</p><p>const reader = response.body.getReader();
let receivedLength = 0;
let targetProgress = 0;
let currentProgress = 0;
let rafId = null;</p><p>// 启动 rAF 平滑更新循环
const updateLoop = () => {
// 缓动:每次更新 1.5% 的差距(可调)
currentProgress += (targetProgress - currentProgress) * 0.015;
// 防止浮点误差导致永远不等于 1
if (Math.abs(targetProgress - currentProgress) < 0.001) {
currentProgress = targetProgress;
}</p><pre class="brush:php;toolbar:false;">// 更新 UI
const percent = Math.min(100, Math.round(currentProgress * 100));
progressBar.style.width = `${percent}%`;
statusText.textContent = `下载中... ${percent}%`;
if (currentProgress < targetProgress) {
rafId = requestAnimationFrame(updateLoop);
}};
// 开始读取流 const pump = async () => { const { done, value } = await reader.read(); if (done) { currentProgress = 1; targetProgress = 1; progressBar.style.width = '100%'; statusText.textContent = '下载完成'; return; }
receivedLength += value.length;
targetProgress = total ? receivedLength / total : 0; // 若无 content-length,用 0–1 区间预估
if (!rafId) {
rafId = requestAnimationFrame(updateLoop);
}
return pump(); // 继续读取};
await pump(); }
补充优化建议
-
防抖初始帧:首次调用 rAF 前加
setTimeout(() => requestAnimationFrame(...), 0),避免与页面加载竞争 - 暂停/恢复支持:在 rAF 循环中检查标志位,暂停时 cancelAnimationFrame,恢复时重新 requestAnimationFrame
-
降级兼容:对不支持 ReadableStream 的环境(如 Safari ≤ 16.4),改用
XMLHttpRequest+onprogress,同样套用 rAF 插值逻辑 -
内存友好:不要 accumulate 全量 ArrayBuffer,用
value读完即弃,避免 OOM



















