为什么你的大文件断点续传,一遇到弱网就崩溃?

在前端面试里,如果问到大文件上传,几乎每个人都能背出那套标准八股文:用 Blob.slice 对文件进行物理切片,然后用 Promise.all 并发上传,最后调一个后端的合并接口。

听起来无懈可击对吧?

但在真实的业务现场,当你的代码跑在高铁上的弱网环境、或是丢包率极高的星巴克公用 Wi-Fi 下时,这套标准答案往往会引发一场场线上灾难。上传进度条会在最后卡死,浏览器页签会突然白屏崩溃,甚至后端网关会直接把你判定为恶意攻击而拉黑。

为什么看似完美的逻辑,一碰到复杂的物理网络就会碎成渣?接下来咱们聊一聊这个话题👇


常规切片上传,有什么问题?

大多数网上的教程,教你的都是纯粹的玩具级代码 。它们最大的问题在于:完全无视了浏览器底层的并发限制和物理网络的脆弱性

当你对一个 2GB 的大文件切出了 100 个分片,然后大手一挥,直接用 Promise.all100 个请求同时砸向后端时,灾难就开始了:

首先,现代浏览器对同一个域名的 TCP 并发连接数是有严格限制的(通常是 6 个)。你发出的 100 个请求,会在浏览器底层网络栈里排起长队,互相挤占极其有限的带宽,导致大量请求在排队阶段就直接报超时错。

其次,在弱网环境下,只要其中有两三个分片因为丢包而上传失败,整个 Promise.all 就会直接进入 reject 状态。前面辛辛苦苦传完的 97 个分片状态瞬间丢失,用户只能被迫从头开始,这种体验简直是毁灭性的。

没有并发控制、没有容错重试、没有状态持久化的切片上传,就是一场灾难的开始。


简单实现原生并发队列与退回重试

真正能上生产环境的上传模块,绝对不应该依赖花里胡哨的第三方大件库,而是要老老实实地用底层的 JavaScript 去精准接管整个上传生命周期。

我们不需要引入庞大的状态机。利用极简的异步任务队列 配合递归控制,就能在纯原生的 Web API 下实现带错误重试、精准并发控制的上传逻辑:

typescript 复制代码
// 大文件切片并发上传队列控制
interface ChunkTask {
  chunk: Blob;
  index: number;
  retryCount: number;
}

export async function uploadChunksWithControl(
  tasks: ChunkTask[],
  maxConcurrency = 4, // 极其克制的并发数,保护网络带宽
  maxRetries = 3      // 弱网环境的重试次数
): Promise<void> {
  let currentIndex = 0;
  let activeCount = 0;
  let hasError = false;

  return new Promise((resolve, reject) => {
    const enqueue = async () => {
      // 如果存在有不可恢复的重试耗尽,或者任务已经发完,直接退出
      if (hasError || currentIndex >= tasks.length) {
        if (activeCount === 0 && !hasError) resolve();
        return;
      }

      // 获取当前要处理的任务,并移动指针
      const task = tasks[currentIndex++];
      activeCount++;

      try {
        await uploadSingleChunk(task);
      } catch (err) {
        // 如果遇到网络抖动,不报错,而是进行静默重试
        if (task.retryCount < maxRetries) {
          console.warn(`分片 ${task.index} 上传失败,正在进行第 ${task.retryCount + 1} 次重试...`);
          task.retryCount++;
          // 将失败任务重新插回队列末尾排队
          tasks.push(task); 
        } else {
          hasError = true;
          reject(new Error(`分片 ${task.index} 弱网重试 ${maxRetries} 次后彻底失败`));
          return; // 掐断后续上传,避免资源浪费
        }
      } finally {
        activeCount--;
        enqueue(); // 递归触发下一个空闲插槽的任务
      }
    };

    // 初始化时,填充并发池,启动指定数量的任务
    for (let i = 0; i < maxConcurrency && i < tasks.length; i++) {
      enqueue();
    }
  });
}

// 模拟单个分片的独立网络请求
async function uploadSingleChunk(task: ChunkTask): Promise<void> {
  const formData = new FormData();
  formData.append('file', task.chunk);
  formData.append('index', task.index.toString());
  
  const res = await fetch('/api/upload-chunk', {
    method: 'POST',
    body: formData,
  });

  if (!res.ok) throw new Error('Network Error');
}

这段代码没有任何魔法。它通过 activeCount 永远将并发请求死死卡在阈值(比如 4)以内。同时,某个分片因为弱网失败了不仅不会导致全盘崩溃,反而会被打回队列重新排队。这种退回策略,是移动端弱网环境下保障上传成功率的前提。


那些在真机上才会暴露的坑

哪怕你写出了上面👆 的并发队列,一旦放到真实的业务线上去跑大文件,依然会碰到只有在深水区才会暴露的死角。这些才是拉开架构师和普通开发差距的地方🙌。

页面静默中崩溃?

很多新手在切片时,喜欢把所有的 Blob 对象转成 ArrayBuffer 或者 Base64 存在一个巨大无比的数组里。如果用户上传的是一个 5GB 的高清视频,这个低级操作会瞬间撑爆浏览器的堆内存,导致页面卡死或直接白屏抛出 OOM

所以咱们 必须清楚一点,File.slice() 产生的只是文件物理内存的指针引用,而不是真实的数据拷贝。在真实网络请求发生之前,绝对不要去调用任何 FileReader 方法读取内容。只有在发请求的瞬间,才将引用丢进 FormData

计算文件哈希时,尽量避免主线程卡死

为了实现断点续传和秒传校验,我们需要用文件的哈希值(如 MD5SHA-256)作为全局唯一标识。但对于几个 GB 的文件,如果用纯 JavaScript 在主线程全量计算哈希,页面会整整卡死几十秒,任何按钮交互都会瘫痪的。

所以咱们对于几十兆以上的大文件,必须丢进 Web Worker 中进行分块异步计算。更高级的工程做法是抽样哈希 (仅抽取文件头部的 2MB、尾部的 2MB 加上中间若干个小片段拼接),这能在保证冲突率极低的前提下,把计算时间从几十秒压缩到几百毫秒。

状态持久化

移动端浏览器极其不靠谱,用户切出去回个微信,你的上传页面可能就被系统杀掉后台了。等用户重新点开页面,内存里的分片状态全部丢失,只能咬牙从头再传。

如果引入 IndexedDBlocalStorage 作为本地状态机时,每成功上传一个切片,就向本地数据库持久化写入一条记录。用户刷新页面后,通过哈希比对本地记录,直接过滤掉已经成功的切片数组,无缝接管中断的上传任务。


大文件断点续传架构图(建议收藏😁)

在大厂的日常业务里,很多听起来高大上的架构优化,其实底层的逻辑非常朴素的。

我们要对抗的从来不是多么深奥的算法机制,而是浏览器可怜的并发限制、脆弱的物理网络,以及设备的物理内存上限。前端工程师不仅要会写代码,更要懂得对底层的物理极限保持敬畏🫡。

当你能把一个巨大无比的文件,在网速只有 50KB/s 并且频繁断网的恶劣环境下,稳稳当当、不漏一片地送到服务器时,你在这个圈子才算是真正摸到了前端工程的门道🫵。

好了,今天就分享到这里🙌

相关推荐
胡哈2 小时前
React Compiler:让开发者忘掉 memo 这件事
前端·react.js
xiyueyezibile2 小时前
“自迭代” Skill 的 team-pitfalls 的演进
前端·后端·ai编程
林恒smileZAZ2 小时前
`Array(100).map(() => 1)` 为什么全为空?
前端·javascript
小巧的砖头3 小时前
C#会重蹈覆辙吗?系列之2:反射及元数据的性能问题
开发语言·前端·c#
KaMeidebaby3 小时前
卡梅德生物技术快报|蛋白的叠氮基修饰:实操解析:核酸模板耦合蛋白的叠氮基修饰实现靶蛋白定点共价标记
前端·人工智能·物联网·算法·百度
濮水大叔3 小时前
在大型 Vue 项目中,如何有效管理复杂的状态?“散装版” vs “套装版”
前端·vue.js·typescript
玄星啊3 小时前
可修改:代码的生存底线
前端·ai编程
没落英雄4 小时前
3. DeepAgents 实战 - Memory Skills 与上下文工程
前端·人工智能·架构
Bigger4 小时前
对不起!我错怪你了,UnoCSS:一次和 AI 一起排查 DevTools 卡顿的经历
前端·css·人工智能