JavaScript分片上传大文件内存占用过高(本应具备内存高效特性)
问题根因
- 你的代码是并行发起所有XHR上传请求,for循环没有等待前一个请求完成就立刻读取下一个分片、发起新请求。所有分片对应的ArrayBuffer会被处于pending状态的XHR实例持有,无法被GC回收,直到请求完成才会释放引用,因此会同时在内存中堆积大量分片,最终占用接近整个文件的内存。
- 你主动调用
file.slice().arrayBuffer()会把分片内容完整读取到JS堆内存的ArrayBuffer对象中,而浏览器的自动GC有触发阈值,只有当内存占用达到对应阈值时才会自动执行,你连续快速创建大量大体积ArrayBuffer时,GC来不及触发就已经把全量分片加载到内存里了。 - 另外XHR的
onreadystatechange回调持有外部作用域的引用,也会一定程度上延缓相关对象的回收时机。
修复方案
你可以按优先级用以下方案优化:
- 直接传递Blob对象给XHR,不要转ArrayBuffer
xhr.send()支持直接接收Blob类型,浏览器底层会自行处理流式读取,不会把整个分片内容加载到JS堆内存,内存占用可以直接降到分片大小级别。 - 改成串行上传,等前一个分片上传完成再处理下一个
把XHR包装成Promise,每次循环await前一个请求完成再执行下一次,避免同时堆积大量请求和分片数据。 - 处理完的分片主动赋值为
null,帮助GC更早标记可回收对象。
修复后的代码示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form id="files"> <input type="file" multiple id="file-input"> <input type="submit"> </form> <script> const form = document.getElementById('files'); form.addEventListener('submit', async (e) => { e.preventDefault(); const input = document.getElementById('file-input'); const chunkSize = 100 * 1024 * 1024; for (let file of input.files) { let start = 0; const nChunks = Math.ceil(file.size / chunkSize); for (let i=0;i < nChunks; i++) { // 直接取Blob,不读ArrayBuffer const slice = file.slice(start, start + chunkSize); start += chunkSize; // 包装XHR为Promise,等待上传完成再走下一个 await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (4 === this.readyState) { console.log(`${i} xhr upload complete`, e); resolve(); } }; xhr.onerror = reject; xhr.open('POST', '/test_memory'); // 直接传Blob xhr.send(slice); }) } } }) </script> </body> </html>
按以上方案修改后,内存会稳定保持在略高于单个分片大小的水平,不会随文件总大小上涨。
内容的提问来源于stack exchange,提问作者ratatatat
相关产品推荐
相关产品推荐

