不能直接用 fetch 下载几 GB 文本文件,因 response.text() 会将整个解码后字符串加载内存,导致主线程卡死、OOM;Worker 中必须用 arrayBuffer 避免解码和跨线程传递问题,并通过换行符对齐分片防止跨行截断,状态需存于 IndexedDB 且键含 etag 与 range。

为什么不能直接用 fetch 下载几 GB 的文本文件
浏览器主线程会卡死,内存暴涨,甚至触发 OOM(Out of Memory)崩溃。尤其当文件是纯文本(如日志、CSV、TSV),内容未压缩、换行符密集时,response.text() 会把整个字符串加载进内存并做 UTF-8 解码,比二进制处理更耗资源。主线程还承担渲染、事件响应等任务,无法安全执行长时间 IO 操作。
Worker 中必须用 arrayBuffer 而非 text 或 blob
在 Web Worker 里调用 fetch 后,应始终选择 response.arrayBuffer() 获取原始字节流。原因有三:
-
response.blob()在 Worker 中虽可用,但后续调用blob.text()仍会回到主线程解码,失去 Worker 意义 -
response.text()会强制解码并生成巨大字符串,极易撑爆 Worker 内存堆 - 只有
ArrayBuffer可以被SharedArrayBuffer或结构化克隆安全传递,也便于后续按需解析(比如跳过前 N 行、按行切分)
示例关键片段:
const res = await fetch(url, { headers: { Range: `bytes=${start}-${end}` } });
const buffer = await res.arrayBuffer(); // ✅ 正确
// const text = await res.text(); // ❌ 危险
// const blob = await res.blob(); // ⚠️ 后续再 .text() 仍危险分片合并时如何避免跨行截断(尤其 CSV/TSV)
文本文件的致命陷阱是:某一行恰好横跨两个分片边界。若简单拼接 ArrayBuffer,会导致该行被硬生生切断,后续解析全错。解决思路不是“修复”,而是“规避”——让每个分片末尾自动延伸到下一个换行符。
- 下载完一个分片后,在其末尾向前搜索最近的
\n(或\r\n),将该位置设为实际有效截止点 - 下一分片从该位置 +1 开始,而非严格按字节对齐;这样能保证每块都以完整行为单位结束
- 首块无需前置补全,末块无需特殊处理——所有中间块都“多拿少算”,最终总长度仍等于原文件
伪代码逻辑:
function adjustChunkEnd(buffer, start, originalEnd) {
const view = new Uint8Array(buffer);
for (let i = view.length - 1; i >= 0; i--) {
if (view[i] === 10 || view[i] === 13) return start + i; // \n or \r
}
return originalEnd; // fallback
}断点状态必须存在 IndexedDB,且键要含 etag + range
仅靠 localStorage 存储进度不可行:5MB 上限太小,且无法存二进制数据。IndexedDB 是唯一可行方案,但设计不当会导致状态错乱:
- 每条记录的 key 应为
${etag}_${start}-${end},而非单纯chunk_1——防止同名文件不同版本覆盖 - value 存储
{ buffer: ArrayBuffer, timestamp: number },不存 Blob(Blob 无法结构化克隆进 IDB) - 合并阶段按
start排序读取所有已存 chunk,逐个new Uint8Array(buffer)拼接,最后用new Blob([...allUint8Arrays], {type: 'text/plain'})输出
容易忽略的一点:etag 必须从 HEAD 响应头读取,且服务端需开启 ETag 和 Accept-Ranges: bytes,否则断点续传无意义。
真正难的不是并发请求,而是确保每一字节都落在正确语义位置——文本文件没有“块校验和”,只有靠行边界对齐和 etag 版本锁定,才能让几 GB 的下载结果可信赖。

















