浏览器大文件切片上传崩溃主因是未释放Blob URL及隐式强引用致内存堆积,需配对revoke、避免保存原始File引用、用transferable移交Worker、主动创建独立Blob副本。

浏览器处理海量大文件切片上传时,崩溃往往不是因为“内存爆了”,而是因为大量未释放的 Blob URL 和隐式强引用导致内存持续堆积,最终触发进程级内存限制(如 Chrome 的 4GB 堆上限或 OOM Killer)。关键不在“回收时机”,而在“不给 GC 回收的机会”——只要 URL 没 revoke、Blob 被变量长期持有、或切片对象里存着原始 File 引用,GC 就无法清理底层二进制数据。
Blob URL 必须配对 revoke,不能靠页面卸载兜底
每次调用 URL.createObjectURL(blob) 都会创建一个强引用,阻止该 Blob 被回收,哪怕你已经把 blob 变量设为 null 或离开作用域。单页应用中切换路由、反复预览文件、重选上传项时,极易累积几十甚至上百个未释放 URL。
- 切片上传中若用于临时预览(如视频帧截图、PDF 第一页渲染),每生成一个 preview URL,必须在预览结束或切片发送完成后立刻
URL.revokeObjectURL(url) - 不要写成
url = URL.createObjectURL(slice); ... // 后续忘了 revoke;推荐封装安全函数:function createTempUrl(blob) { const url = URL.createObjectURL(blob); return { url, cleanup: () => URL.revokeObjectURL(url) }; }
然后在 Promise resolve 或 error 后统一调用cleanup() - 监听组件卸载(如 React 的
useEffectcleanup、Vue 的beforeUnmount)作为兜底,但不能作为主逻辑——用户可能长时间停留、反复操作,卸载事件根本不会触发
切片对象别保存原始 File 或完整 Blob 引用
File 是 Blob 的子类,但带 name、lastModified 等元数据;一旦你在切片数组里存的是 { slice: file.slice(...), file },整个原始 File 对象(及其背后的数据句柄)就会被间接持住——哪怕只用了其中 1 个切片,整份文件都驻留内存。
- 上传前把 File 转成纯 Blob:
const blob = new Blob([file], { type: file.type }),再基于这个 blob 切片;丢弃对原始file的所有引用 - 切片对象只保留必要字段:索引、二进制数据(slice)、大小、校验 hash(如可选),避免挂载
originalFile、parentBlob这类强引用字段 - 若需文件名或类型,提取后存字符串,而不是保留对象引用:
fileName: file.name, mimeType: file.type
用 transferable 传递切片,尤其配合 Web Worker
当切片需送入 Web Worker 计算 MD5、SHA-1 或加密时,若直接传 Blob,浏览器会复制二进制数据——不仅耗时,还双倍占用内存。而使用 postMessage(blob, [blob]) 的 transfer list,可将 Blob 所有权移交 Worker,主线程立即失去引用,GC 可马上回收(前提是没其他引用)。
- Worker 内处理完后,也应尽快释放其 own 的 URL(如有)和中间 Blob
- 主线程收到 hash 结果后,不要再把切片 Blob 重新赋值给某个长期存活对象(如 class 实例属性、全局 map)
- 注意:transfer 后主线程的 Blob 变成“已转移状态”,不可再读取或调用
slice(),否则报错 —— 这反而是内存安全的保障
主动切断 slice 与原始 Blob 的底层数组绑定
Blob.prototype.slice() 返回的新 Blob 仍共享原始 Blob 的底层存储(类似 Go 切片),尤其当原始文件极大、而你只取前几 KB 做 header 校验时,整个文件数据仍被锁定。这不是 bug,是设计使然,但必须主动干预。
- 校验完头部后,若无需整文件,用
new Blob([slice])创建独立副本 —— 构造新 Blob 会触发底层数据拷贝(异步且受控),释放原始引用 - 对关键切片(如首片、末片),可显式剥离:
const standalone = new Blob([await slice.arrayBuffer()], { type: slice.type }) - 避免链式 slice:不要
blob.slice(0, 100).slice(10, 20),这仍绑定最外层;直接blob.slice(10, 20)


















