一个 4.7 GB 视频把浏览器拖进 OOM

你给一家在线教育平台做「课程视频批量上传」功能。

需求听起来很朴素:讲师后台一次性拖 20 个 4K 视频,浏览器要稳、要快、要能断网续传。

你第一版直接 <input type="file"> + FormData,结果上线当天就炸:

  • 讲师 A 上传 4.7 GB 的 .mov,Chrome 直接 内存溢出 崩溃;
  • 讲师 B 网断了 3 分钟,重新上传发现进度条归零,心态跟着归零;
  • 运营同学疯狂 @ 前端:"你们是不是没做分片?"

解决方案:三层防线,把 4 GB 切成 2 MB 的"薯片"

1. 表面用法:分片 + 并发,浏览器再也不卡

js 复制代码
// upload.js
const CHUNK_SIZE = 2 * 1024 * 1024;    // 🔍 2 MB 一片,内存友好
export async function* sliceFile(file) {
  let cur = 0;
  while (cur < file.size) {
    yield file.slice(cur, cur + CHUNK_SIZE);
    cur += CHUNK_SIZE;
  }
}
js 复制代码
// uploader.js
import pLimit from 'p-limit';
const limit = pLimit(5);               // 🔍 最多 5 并发,防止占满带宽
export async function upload(file) {
  const hash = await calcHash(file);   // 🔍 秒传、断点续传都靠它
  const tasks = [];
  for await (const chunk of sliceFile(file)) {
    tasks.push(limit(() => uploadChunk({ hash, chunk })));
  }
  await Promise.all(tasks);
  await mergeChunks(hash, file.name);  // 🔍 通知后端合并
}

逐行拆解:

  • sliceFilefile.slice 生成 Blob 片段,不占额外内存
  • p-limit 控制并发,避免 100 个请求同时打爆浏览器;
  • calcHash 用 WebWorker 算 MD5,页面不卡顿(后面细讲)。

2. 底层机制:断点续传到底续在哪?

角色 存储位置 内容 生命周期
前端 IndexedDB hash → 已上传分片索引数组 浏览器本地,清缓存即失效
后端 Redis / MySQL hash → 已接收分片索引数组 可配置 TTL,支持跨端续传
sequenceDiagram participant F as 前端 participant B as 后端 F->>B: POST /prepare {hash, totalChunks} B-->>F: 200 OK {uploaded:[0,3,7]} loop 上传剩余分片 F->>B: POST /upload {hash, index, chunkData} B-->>F: 200 OK end F->>B: POST /merge {hash} B-->>F: 200 OK Note over B: 按顺序写磁盘
  1. 前端先 POST /prepare 带 hash + 总分片数;
  2. 后端返回已上传索引 [0, 3, 7]
  3. 前端跳过这 3 片,只传剩余;
  4. 全部完成后 POST /merge,后端按顺序写磁盘。

3. 设计哲学:把"上传"做成可插拔的协议

ts 复制代码
interface Uploader {
  prepare(file: File): Promise<PrepareResp>;
  upload(chunk: Blob, index: number): Promise<void>;
  merge(): Promise<string>;            // 🔍 返回文件 URL
}

我们实现了三套:

  • BrowserUploader:纯前端分片;
  • TusUploader:遵循 tus.io 协议,天然断点续传;
  • AliOssUploader:直传 OSS,用 OSS 的断点 SDK。
方案 并发控制 断点续传 秒传 代码量
自研 手动 自己实现 手动 300 行
tus 内置 协议级 需后端 100 行
OSS 内置 SDK 级 自动 50 行

应用扩展:拿来即用的配置片段

1. WebWorker 算 Hash(防卡顿)

js 复制代码
// hash.worker.js
importScripts('spark-md5.min.js');
self.onmessage = ({ data: file }) => {
  const spark = new SparkMD5.ArrayBuffer();
  const reader = new FileReaderSync();
  for (let i = 0; i < file.size; i += CHUNK_SIZE) {
    spark.append(reader.readAsArrayBuffer(file.slice(i, i + CHUNK_SIZE)));
  }
  self.postMessage(spark.end());
};

2. 环境适配

环境 适配点
浏览器 需兼容 Safari 14 以下无 File.prototype.slice(用 webkitSlice 兜底)
Node fs.createReadStream 分片,Hash 用 crypto.createHash('md5')
Electron 渲染进程直接走浏览器方案,主进程可复用 Node 逻辑

举一反三:3 个变体场景

  1. 秒传
    上传前先算 hash → 调后端 /exists?hash=xxx → 已存在直接返回 URL,0 流量完成。
  2. 加密上传
    uploadChunk 里加一层 AES-GCM 加密,后端存加密块,下载时由前端解密。
  3. P2P 协同上传
    用 WebRTC 把同局域网学员的浏览器变成 CDN,分片互传后再统一上报,节省 70% 出口带宽。

小结

大文件上传的核心不是"传",而是"断"。

把 4 GB 切成 2 MB 的薯片,再配上一张能续命的"进度表",浏览器就能稳稳地吃下任何体积的视频。

相关推荐
寅时码14 分钟前
React 之死·终章:一个 useRef,把闭包陷阱、依赖数组、漫天 rerender 全送走
前端·react.js·ai编程
不好听61318 分钟前
HTML 事件监听机制:从 DOM Level 0 到 React 合成事件
前端·react.js·html
wordbaby26 分钟前
为什么刷新页面就 404?聊聊 SPA 路由与 Nginx 的那点事
前端
daols8832 分钟前
vxe-table 渲染器教程一:实现金额输入控件
javascript·vue.js·vxe-table
不好听6131 小时前
React 核心概念:JSX、组件与数据驱动
前端·react.js
触底反弹1 小时前
🔥 React 零基础入门(中):500 行屎山代码到组件化的蜕变
前端·javascript·react.js
不好听6131 小时前
React vs Vue:两大前端框架技术选型深度对比
前端·vue.js·react.js
GuWenyue1 小时前
Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%
前端·数据库·人工智能
GuWenyue1 小时前
传统Agent工具两大痛点!300行代码落地MCP跨语言工具,彻底解耦LLM与工具
前端·人工智能·算法
小林ixn2 小时前
从 onclick 到 React 合成事件,再到完整应用逻辑:一次前端架构的深度剖析
前端·react.js·前端框架