节流不是为了减慢上传,而是让进度UI更稳更顺——避免高频重绘导致卡顿掉帧;推荐用requestAnimationFrame节流,确保每帧最多刷新一次,记录最新进度值;时间戳版节流适用于fetch分片场景,设定最小更新间隔并重置计时器;进阶优化包括95%后缩短节流间隔、CSS过渡动画、不确定大小时用不确定样式及无障碍支持。

节流不是为了减慢上传,而是让进度 UI 更稳、更顺——避免每毫秒都重绘一次进度条,导致卡顿或掉帧。
为什么必须节流?
XMLHttpRequest.upload.onprogress 或 fetch + ReadableStream 手动计算进度时,事件可能每几毫秒就触发一次。尤其在千兆网络或本地测试环境下,1 秒内触发上百次回调很常见。如果每次回调都直接改 DOM(比如设置 style.width 或更新文本),浏览器会频繁重排重绘,主线程压力陡增,UI 反而显得“抖”或“滞后”。
推荐用 requestAnimationFrame 节流
这是最轻量、最契合渲染节奏的方式:不丢精度、无延迟堆积、天然防重复提交。
- 只记录最新进度值,不立即更新 UI
- 用
requestAnimationFrame确保每帧最多刷新一次 - 即使进度快速跳变(如从 42% 突然到 48%),最终显示的仍是最新值
示例代码:
立即学习“Java免费学习笔记(深入)”;
let currentPercent = 0;
let isPending = false;
function updateProgress(percentage) {
currentPercent = Math.min(100, Math.max(0, percentage));
if (!isPending) {
isPending = true;
requestAnimationFrame(() => {
document.getElementById('bar').style.width = currentPercent + '%';
document.getElementById('text').textContent = Math.round(currentPercent) + '%';
isPending = false;
});
}
}
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
updateProgress((e.loaded / e.total) * 100);
}
};
时间戳版节流适合 fetch 分片场景
当用 fetch 手动分块上传(如每 chunk 传完调一次进度),回调密度可控但依然偏高,可用时间间隔控制:
- 设定最小更新间隔(如 100–200ms)
- 每次有新进度,就重置计时器,丢弃中间旧值
- 比 rAF 更适合需要明确“最低刷新节奏”的后台任务
示例:
function throttleUpdate(fn, delay = 160) {
let last = 0;
return (data) => {
const now = Date.now();
if (now - last >= delay) {
fn(data);
last = now;
}
};
}
const throttled = throttleUpdate(updateProgressUI);
uploadChunk(chunk).then(() => {
throttled({ loaded: uploadedSoFar, total: fileSize });
});
进阶优化点
真实项目中,光节流还不够:
- 95% 后可自动缩短节流间隔(如切到 50ms),避免最后阶段“卡住不动”
- 用 CSS
transition: width 0.2s ease-out让变化更柔和,减少视觉跳跃 - 对不确定总大小的上传(
e.total === 0),改用 indeterminate 样式或动画条,避免显示“0%”误导用户 - 添加
aria-valuenow和role="progressbar",保障读屏软件可读性


















