XMLHttpRequest(JS/React)分片上传大文件时浏览器崩溃的解决办法问询
问题根因
- 现有实现会一次性将整个文件读入内存:调用
reader.readAsArrayBuffer(file)会把完整的文件内容全量加载到内存中,文件越大内存占用越高,超过1G很容易触发浏览器内存限制直接崩溃 - 使用了同步XHR请求:
xhr.open('PUT', url, false)第三个参数为false代表同步请求,会阻塞浏览器主线程,上传过程中页面会完全卡死,甚至触发浏览器无响应提示 forEach搭配async的写法完全无效:所有分片上传请求会同时发起,大量请求同时持有分片内存,进一步加剧内存占用
优化实现方案
优化核心思路是不需要一次性读完整文件,利用File本身是Blob类型的特性直接分片切割后上传,同时控制并发数,使用异步请求避免阻塞主线程,修改后代码如下:
axios.post(`/URL_GOES_HERE`, { name: file.name, size: file.size, md5, extension: file.type.split('/')[1] }).then(async (response) => { const { chunks, status } = response.data; if (status !== 'uploading') return; // 可根据实际场景调整并发上限,避免请求过多占用内存 const CONCURRENT_LIMIT = 3; let start = 0; // 生成分片上传任务队列 const chunkTasks = chunks.map(({ materialId, size, index }) => { // 直接切割File,不需要提前读取全量文件 const chunk = file.slice(start, start + size); start += size; return async () => { const url = `URL_FOR_EACH_CHUNK`; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); // 第三个参数为true,使用异步请求不阻塞主线程 xhr.open('PUT', url, true); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('Authorization', `Bearer TOKEN_GOES_HERE`); xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(xhr.status); xhr.onerror = reject; // 直接发送Blob分片,不需要额外转Uint8Array减少内存拷贝 xhr.send(chunk); }); }; }); // 控制并发执行 const executing = []; for (const task of chunkTasks) { const p = task(); executing.push(p); // 达到并发上限就等待任意任务完成再补入下一个任务 if (executing.length >= CONCURRENT_LIMIT) { await Promise.race(executing); executing.splice(executing.indexOf(Promise.race(executing)), 1); } } // 等待所有剩余分片上传完成 await Promise.all(executing); // 此处可补充全部分片上传完成后的后续逻辑 })
关键优化说明
- 移除了全量读取文件的
FileReader逻辑:Blob.slice(File继承自Blob)本身支持直接切割文件,不需要提前把整个文件加载到内存,浏览器会自动按需读取对应分片内容,内存占用可以稳定在和分片大小相近的量级 - 全部改用异步XHR请求,不会阻塞主线程,上传过程中页面操作不会受到影响
- 增加了并发数控制,默认同时上传3个分片,可根据业务场景灵活调整数值,避免同时发起几十上百个请求导致内存暴涨
- 直接发送Blob类型的分片,不需要额外转成
Uint8Array,减少不必要的内存拷贝
内容的提问来源于stack exchange,提问作者MatiN
相关产品推荐
相关产品推荐

