JS 进度条 6 种实现技巧:假进度、rAF 动画、真实字节与无障碍写法
进度条看起来是简单组件,实际是前端里少数“要同时懂 CSS、定时器、异步 API 和可访问性”的小综合体。本文给出 6 种真实可用的实现方式(从纯 CSS 动画到 requestAnimationFrame、setInterval 模拟、Fetch 流式读取真实字节进度、Promise 并发任务进度、以及原生 <progress>),并说明各自的适用边界与常见 bug。

一、基础结构:宽度百分比驱动
<div class="bar"><div class="bar__fill" id="fill"></div></div>
<style>
.bar { height: 10px; background: #eef1f5; border-radius: 5px; overflow: hidden; }
.bar__fill { height: 100%; width: 0; background: linear-gradient(90deg,#4f7cff,#39c2ff);
transition: width .25s ease; }
</style>
核心一行就是 fill.style.width = percent + '%'。加 transition 会让进度变化看起来顺滑;但注意如果每次更新间隔小于过渡时长,动画会显得滞后,真实高频更新场景应把 transition 调短或去掉。
二、setInterval 模拟进度(不确定耗时)
function fakeProgress(el, { cap = 92, step = 1, ms = 120 } = {}) {
let p = 0;
const t = setInterval(() => {
if (p >= cap) return clearInterval(t); // 卡在92%等真实结果
p += Math.random() * step + 0.2;
el.style.width = Math.min(p, cap) + '%';
}, ms);
return { done: () => { clearInterval(t); el.style.width = '100%'; } };
}
用于“无法获知真实进度”的接口。关键是封顶值不要到 100:真结束时再一次性推到 100%,用户才不会觉得卡在 99% 是 bug。
三、requestAnimationFrame:真正流畅的动画
function animate(el, from, to, duration = 600) {
const t0 = performance.now();
(function frame(now) {
const k = Math.min(1, (now - t0) / duration);
const ease = 1 - Math.pow(1 - k, 3); // easeOutCubic
el.style.width = (from + (to - from) * ease) + '%';
if (k < 1) requestAnimationFrame(frame);
})(t0);
}
比 setInterval 更省电更跟手,且自动与屏幕刷新同步。页面切到后台时 rAF 会暂停,回来继续,不会疯狂补帧。
四、用 XHR 拿真实上传进度(最可靠)
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
fill.style.width = (e.loaded / e.total * 100).toFixed(1) + '%';
label.textContent = (e.loaded / 1024 / 1024).toFixed(1) + ' MB';
}
};
xhr.onload = () => fill.style.width = '100%';
xhr.onerror = () => bar.classList.add('is-error');
xhr.open('post', '/upload');
xhr.send(file);
upload.onprogress 的 loaded/total 是真实字节数,这是上传进度条唯一正确的做法。Fetch 的 ReadableStream 只能拿到“已接收”而难拿到总大小,因此下载进度更适合用 XHR 或先取 Content-Length。
五、并发任务进度:Promise.all 的问题与正确写法
async function runWithProgress(tasks, onTick) {
let done = 0;
await Promise.allSettled(tasks.map(t => t().finally(() => {
done++; onTick(done / tasks.length); // 每完成一个推进一格
})));
}
用 allSettled 而不是 all:单个任务失败不应导致进度条永远停在中间。按“已完成数量”推进度比按时间估算更诚实。
六、纯 CSS 循环进度(加载指示器)
.loader { width: 100%; height: 10px; overflow: hidden; background: #eef1f5; }
.loader::before { content: ""; display: block; height: 100%; width: 30%;
background: #4f7cff; animation: run 1.2s infinite ease-in-out; }
@keyframes run { 0%{transform:translateX(-100%)} 100%{transform:translateX(340%)} }
适合“不知道进度”的场景(如首屏资源加载)。用 transform 而非改 left,走 GPU 合成不触发重排。
七、别忘了原生 <progress> 与无障碍
<progress id="pg" max="100" value="0" aria-label="文件上传进度"></progress>
<output for="pg">0%</output>
<script>
pg.value = 45; // 原生进度条
pg.setAttribute('aria-valuenow', 45);
</script>
自绘进度条必须补 role="progressbar"、aria-valuemin/max/now、aria-label,否则读屏软件完全读不出来。百分比文字与条形同时给出,色盲用户也能靠文字判断。
八、五个高频 bug
- 进度到 100% 后界面卡住:真实结果还没回来,应区分“进度完成”和“任务成功”。
- 除零错误:
e.total为 0 或未定义时先判lengthComputable。 - 浮点累加误差:用总字节或完成数计算,别把小增量一路相加。
- 频繁写 DOM:每帧更新一次即可,高频数据先缓存变量再由 rAF 统一渲染。
- 宽度百分比溢出:
min(100, val)夹一层,避免 transform 超出容器。
选型建议:上传/下载用字节真实进度;批量异步任务用完成计数;无法预估的用封顶假进度;纯等待态用CSS 循环条;数值动画一律走 requestAnimationFrame,同时把 ARIA 属性补齐。
生产内容,同时打通搜索引擎(SEO)与AI搜索引擎(GEO)流量入口的软件! 点击进入365建站器 »
