大文件分片上传的五层追问从 Blob 切片到秒传与弱网容错

分片上传的坑不在切文件,在哈希策略、断点存储选型、并发与重试、Blob 内存释放。第一层人人会,第二层开始分层,能讲到第三层才算合格。

"你做过大文件上传吗?完整流程讲一下。"

候选人答得很顺:分片切割 Blob、后端合并、断点续传、秒传,关键词一个没漏。

面试官接着追问:从零搭一套生产可用的"分片 + 断点续传 + 秒传",从文件读取、切片策略、并发控制、异常重试、服务端校验、缓存秒传、弱网容错到浏览器存储上线,全链路的坑和优先级怎么排?

当场卡壳。

这不是候选人菜,是题目跨度太大。File.slice() 到 IndexedDB 内存回收、到分布式存储去重,中间隔了五层认知。下面一层层拆。

第一层:slice 谁都会,追问问的是边界

基础三件套:Blob / File / FileReader,加一个 slice。固定大小切片,前端循环发请求,后端收分片最后合并。写出来大概是这样:

javascript 复制代码
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
const chunks = [];
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
  chunks.push(file.slice(start, start + CHUNK_SIZE));
}

代码没错,但面试官想听的是三个追问。

切片多大合适? 没有银弹。切太小,请求数爆炸、TCP 握手和 header 开销被放大;切太大,一个 10MB 分片传到 99% 断了就是白传,重试成本极高。经验做法是让单片传输时间落在 3-10 秒区间,常见 2-5MB,超大视频给到 5-10MB。

slice 边界怎么处理? File.slice(start, end) 会自动 clamp,start 超过 file.size 返回空 Blob,不会抛错。真正的坑在拼接端:最后一个分片通常不是整倍数,后端必须按分片序号而不是字节偏移拼接。

localStorage 存断点记录有上限吗? 有,多数浏览器单域名约 5MB,且是同步写入,会阻塞主线程。超限不报错,直接静默丢失------这是最阴的一类 bug。

这一层能用,但经不起追问。

第二层:哈希、并发、清理------大部分人卡死在这

文件身份证怎么造

秒传和断点续传都依赖一个"文件身份证"。全量 MD5 最准,但一个 2GB 文件在主线程算哈希,页面直接冻住。工程上更常用采样哈希:取文件首尾各一段 + 中间若干等距片段组合计算,速度快几个数量级,碰撞概率可接受。真要求强一致,就把全量哈希丢进 Web Worker。

javascript 复制代码
// spark-md5 增量计算,首尾 + 中间采样
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
reader.onload = (e) => {
  spark.append(e.target.result);
  const fileHash = spark.end(); // 作为秒传标识
};
// 只读前 2MB,别整个文件读完
reader.readAsArrayBuffer(file.slice(0, 2 * 1024 * 1024));

并发不是越大越快

HTTP/1.1 同域 TCP 连接数通常约 6,超过就是排队,还可能触发网关限流。控制在 3-5 个并发是比较稳的区间,同时要留降级通道:收到 429 或 503 就降低并发、拉长间隔。

javascript 复制代码
async function uploadWithLimit(tasks, limit = 4) {
  const pool = new Set();
  for (const task of tasks) {
    const p = task().finally(() => pool.delete(p));
    pool.add(p);
    if (pool.size >= limit) await Promise.race(pool);
  }
  await Promise.all(pool);
}

断点记录存在哪,取决于它要活多久

刷新页面和关闭浏览器,存活周期完全不同,混用一套存储是常见错误。

方案 容量 存活周期 适用场景
内存变量 --- 页面级 同页暂停后继续
sessionStorage ~5MB 标签页级 刷新后续传
localStorage ~5MB 持久 关浏览器后重进续传
IndexedDB 数百 MB 持久 超大文件、暂存 Blob

刷新页面用 sessionStorage 就够,关浏览器场景必须上 localStorage 或 IndexedDB。 后者还要处理多标签页并发写同一份断点记录的冲突。

校验与清理

三种校验各管一段事:文件级哈希 判秒传、分片 MD5 保传输完整、服务端合并后再算一次全量哈希防拼接错位。

重复分片和残缺分片必须清理。后端按 fileHash + chunkIndex 做幂等,同一分片重复上传直接覆盖;分片目录挂 TTL(比如 24 小时),定时任务回收没人认领的孤儿分片,否则磁盘会被断掉的上传任务慢慢吃满。

第三层:三种续传场景才是分水岭

普通开发和资深工程师的差距,就在这里------同一个"断点续传",三种场景的处理逻辑根本不是一套

flowchart TD A[选择文件] --> B[计算文件哈希] B --> C{服务端已存在} C -->|命中| D[秒传直接返回] C -->|未命中| E[Worker 内切片] E --> F[并发上传分片] F --> G{分片请求结果} G -->|失败| H[指数退避重试] H --> F G -->|成功| I[记录断点进度] I --> J{分片是否传完} J -->|否| F J -->|是| K[请求服务端合并] K --> L[校验完整文件哈希] L --> M[上传完成]

拖拽暂停 靠内存态即可,取消未发出的请求、保留已传分片记录。刷新续传要先把进度落到 sessionStorage,重新初始化时拉取服务端已存在的分片列表做差集。

切换网络续传最麻烦:移动端从 WiFi 切到 4G,正在飞的分片会超时,必须靠"服务端已存在分片列表"这个权威数据源重新对齐,而不是信任本地记录。

秒传的核心就一句话:带上文件哈希问服务端,已存在则 0 分片直接返回成功。 但它有一个隐藏前提------不同用户上传同一文件时,要考虑是否共用物理存储,否则会有越权访问的安全问题。

上传体的两种形态差异值得记牢:

维度 FormData 裸二进制流
体积开销 multipart 边界和 header 有额外字节 更小
服务端解析 框架自动处理 需按 body 手动读流
内存表现 大文件容易多次拷贝 更适合流式、低内存
兼容性 最好 需注意 Content-Type

进度条别用"已完成分片数 / 总分片数"算,因为分片大小不均且重试会重复计数。正确做法是 已成功分片的字节总和 / file.size,单分片进度用 xhr.upload.onprogress 拿。

多文件同时上传要上队列调度,别让 10 个文件同时开 4 并发,那样谁都在等,体验极差。给每个文件分配权重,串行调度文件、文件内并行传分片。

第四层:页面卡不卡,看 Worker 和内存账

到了生产级,重点从"能不能传"转到"页面会不会卡死"。

切片和算哈希都该进 Web Worker。 主线程上对一个 2GB 文件做 slice 循环加 MD5 计算,帧率直接掉到个位数。Worker 里算完把分片索引和哈希丢回主线程,UI 全程无感。

IndexedDB 替代 localStorage 存断点。 localStorage 5MB 上限连一张大图的断点都存不下,而 IndexedDB 能存数百 MB,还能直接放 Blob。代价是 API 异步且啰嗦,建议封一层 Promise。

预校验拦住无效请求。 文件类型、大小上限、甚至前端侧的基础病毒特征扫描,都在上传前做。一个 4GB 的 .exe 传完才被后端拒掉,浪费的是用户流量和你的带宽。

图片先压缩再分片。 前端 canvas 压缩率通常能砍掉 50% 以上,图片场景没必要原图直传。

暂停、取消、删除要联动。 取消要 abort 在飞的 XHR、清掉定时器、按策略决定是否保留已传分片。删除本地记录时别忘了同步通知服务端清理,否则又变成孤儿分片。

跨域和鉴权。 分片请求同样要带 token,大文件场景建议用请求签名而非长 token------签名有时效,泄露风险低。批量分片请求注意别把签名算进 body 导致每次都要重算。

第五层:TCP 连接、Blob 泄漏、重试退避

最后这层触及浏览器内核和服务端协议栈。

大量分片请求会顶到 TCP 连接数上限。 HTTP/1.1 同域约 6 个连接,20 个并发分片请求里有 14 个在排队。用 HTTP/2 多路复用能缓解,但服务端也要支持。并发数控制本质上就是在跟这个限制博弈。

Blob 内存泄漏是隐形杀手。 每次 URL.createObjectURL() 都会在内存里留一份引用,用完必须手动释放:

javascript 复制代码
const url = URL.createObjectURL(blob);
// 用完立刻释放
URL.revokeObjectURL(url);

频繁读取、释放二进制大对象而不回收,Chrome 的内存曲线会一路爬升直到标签页崩掉。大文件上传尤其要注意分片用完即弃。

重试策略要区分错误类型。 4xx 是请求本身有问题,重试没意义;5xx 和网络超时才重试。重试走指数退避加随机抖动,避免所有分片在同一时刻集体重试把服务端打垮:

javascript 复制代码
const delay = Math.min(base * 2 ** attempt + Math.random() * 1000, 30000);

为什么大文件优先用二进制流而不是 FormData? 因为 FormData 在构造时对大 Blob 往往有一次额外的内存拷贝,2GB 文件能瞬间吃掉几 GB 内存。裸流传输能边读边发,内存占用平稳得多。

长列表上传用 requestIdleCallback 优化。 上百个文件的上传队列渲染会拖慢主线程,把非紧急的状态更新丢进空闲回调,页面滚动和点击才跟手。

写在最后

同一个上传需求,有人只会 input.files[0] 直接 POST,有人能聊主线程调度、内存回收、分布式去重和弱网全链路容错。差距不在会不会用 API,而在有没有真正踩过那些坑。

你们团队做大文件上传时,是自建这套分片合并服务,还是直接上对象存储的分片接口?评论区聊聊踩过的坑。

有用的话点个赞。

相关推荐
Htr_1 小时前
Anysite.io 使用指南:把整个 Web 变成 AI 智能体的数据库
前端·数据库·人工智能
知兀1 小时前
【前端】受控和非受控组件
前端·javascript·react
玉宇夕落1 小时前
别再把 SSE 当普通流式了!从 EventSource 到 LangChain,一次讲透实时推送与流式输出
前端
CappuccinoRose1 小时前
专项事件体系
前端·学习·网络状态·表单事件·剪贴板事件·拖放事件·页面可见性
李剑一1 小时前
各位产品经理求你们优化一下吧,暗黑模式最大的BUG!文中附解决方案
前端
晚安日记wanna1 小时前
只会答加索引MySQL 调优还能聊这 7 个点
数据库·面试
huali2 小时前
vue-split-screen:让 Vue Router 的导航轨迹变成两个页面
前端·javascript·vue.js
bug总结2 小时前
uniapp总结
开发语言·前端·javascript
云运维笔记2 小时前
华为VRP系统文件管理全攻略
前端·华为