最可靠方案是用SVG配合XMLHttpRequest的upload.onprogress事件:通过stroke-dasharray设周长、stroke-dashoffset动态计算偏移(offset = circumference × (1 − percent)),并加rotate(-90)对齐起点,禁用conic-gradient和Canvas以避免卡顿、失真或抖动。

用 SVG + XMLHttpRequest 监听上传进度最可靠
文件上传时的环形进度必须实时响应 upload.onprogress 事件,不能靠定时器模拟。SVG 方案在这里比 CSS conic-gradient 或 Canvas 更稳:它不依赖像素绘制、缩放不失真、动画由浏览器原生调度,且 DOM 节点少,更新开销低。
关键不是“画圆”,而是把 <circle> 当作一条闭合路径,用 stroke-dasharray 和 stroke-dashoffset 控制“已绘制长度”。上传每触发一次 onprogress,就重算偏移值并写入内联样式。
- 必须用
XMLHttpRequest(而非fetch),因为只有它暴露upload.onprogress事件 -
stroke-dasharray值应等于周长(2 * Math.PI * r),建议 JS 计算后注入,避免硬编码误差 - 务必加
transform="rotate(-90)",否则 0% 进度从 3 点钟方向开始,视觉上像“卡住” - 给进度
<circle>加style="transition: stroke-dashoffset 0.15s linear",避免跳变
stroke-dashoffset 的计算逻辑别写反
常见错误是把公式记成 offset = circumference * percent / 100,结果进度越涨 offset 越大,圆反而消失。正确逻辑是:offset 表示“实线段起点向后挪多少”,所以初始全空时 offset = circumference,满进度时 offset = 0。
上传中实时计算应为:
立即学习“前端免费学习笔记(深入)”;
const circumference = 2 * Math.PI * radius; const percent = (event.loaded / event.total) * 100; const offset = circumference - (percent / 100) * circumference;
注意:event.loaded 和 event.total 在上传开始后才稳定;若 event.total === 0(如服务器未返回 Content-Length),则无法计算精确百分比,此时建议 fallback 到 loading 动画(如无限旋转)。
为什么不用 conic-gradient 实现上传进度
conic-gradient 看似简洁,但有三个硬伤直接排除在上传场景外:
- 它无法响应
onprogress的细粒度变化——CSS 变量更新会触发重排,高频写入(如每 50ms 一次)极易卡顿 - 进度 0% 时
calc(0 * 3.6deg)导致整个渐变透明,环“突然消失”,不符合上传中“正在连接→开始传输”的状态语义 - 必须配合
background-clip: content-box和精确padding才能空心,而上传组件常需动态宽高适配,padding值难对齐
除非你只做静态加载指示器(如“正在初始化”),否则别碰 conic-gradient 做上传进度。
Canvas 方案在上传中容易翻车的点
Canvas 虽可完全控制绘图,但上传进度条里它反而增加复杂度:
- 每次更新都要调用
ctx.clearRect()+ 重绘整圈,CPU 开销明显高于 SVG 属性更新 - 必须手动处理 DPR(设备像素比),否则在 Retina 屏上模糊;而 SVG 是矢量,天生适配
-
arc()的起始/终止弧度要用Math.PI * 1.5和Math.PI * 1.5 + 2 * Math.PI * percent / 100,小数精度误差在低速上传时肉眼可见“抖动” - 没有 CSS
transition,动画得自己用requestAnimationFrame补帧,易和onprogress频率冲突
真正需要 Canvas 的场景是带复杂装饰(如粒子、光晕)或离屏渲染大量数据,纯进度反馈没必要自找麻烦。



















