核心是用两次progress事件的时间差和字节差计算瞬时速度,必须监听xhr.upload;需缓存至少两组loaded/时间戳数据,total为0时无法估算;瞬时速率波动大,恒定假设会导致误差;fetch无原生上传进度,推荐回退至XHR。

用 xhr.upload.addEventListener("progress") 拿到实时字节数和时间戳
预估剩余时间的核心不是“猜”,而是靠两次 progress 事件之间的时间差和字节差算出瞬时速度。必须监听 xhr.upload,而不是 xhr 本身——否则收不到上传阶段的事件。
关键点:
- 每次
progress触发时,记录event.loaded(已上传字节数)和Date.now()(毫秒时间戳) - 至少缓存最近两次数据,才能算出速度;只有一组数据时,先用平均速度兜底(
loaded / elapsed) -
event.total必须存在且大于 0,否则无法算百分比和剩余量——某些服务器或代理会丢掉Content-Length,导致total === 0
为什么不能直接用 setInterval 每秒减 1 秒来倒推
上传速度是波动的,尤其在 WiFi 切换、4G/5G 切换、后台程序抢占带宽时,瞬时速率可能从 2MB/s 掉到 20KB/s。如果按初始速度恒定估算,剩余时间误差会越来越大,甚至出现“还剩 10 秒”结果卡住 2 分钟才动一下。
正确做法是每收到一次 progress 就重算一次:
立即学习“前端免费学习笔记(深入)”;
- 当前已用时间 = 当前时间戳 − 开始时间戳
- 当前已传比例 =
loaded / total - 当前瞬时速率 =
(loaded − prevLoaded) / (now − prevTime)(单位:B/ms) - 剩余时间 ≈
(total − loaded) / 当前速率(单位 ms,再转成分秒)
fetch 没有原生 upload.progress,怎么补
fetch 确实不暴露上传进度,但你可以手动切片 + ReadableStream 控制节奏。不过要注意:这需要后端明确支持分片接收(比如接受 Content-Range 或自定义 header),否则会 400 或 500。
更现实的方案是退回到 XMLHttpRequest —— 它仍是目前唯一稳定、无需后端改造、能拿到 upload.progress 的标准方式。如果你非要用 fetch,只能:
- 把文件
slice()成固定大小块(如 64KB),逐个fetchPOST - 每个块发送前记时间,收到响应后算该块耗时,推导出本段速率
- 用最近 N 块的加权平均速率预估剩余块总耗时
- 缺点:HTTP 请求头开销变大,服务端需做合并逻辑,前端代码复杂度翻倍
显示“剩余时间”时最容易被忽略的三个细节
很多实现把剩余时间直接写成 mm:ss,但用户真正需要的是可读性 + 可信感。这三个点不处理,数字再准也显得假:
- 当剩余时间
- 速率低于 1KB/s 时,剩余时间误差极大,建议改用模糊提示:“上传较慢,预计还需几分钟”
- 不要每帧都更新 DOM——用
requestAnimationFrame节流,或至少限制每 200ms 更新一次,否则频繁重排影响主线程



















