Skip to content

大文件分片上传与并发哈希计算:前端性能优化实践

背景与挑战

在现代Web应用中,处理大文件上传是常见需求。但直接上传大文件会面临:

  • 网络不稳定导致上传失败
  • 服务器内存压力过大
  • 用户等待时间过长
  • 无法实现断点续传

分片上传技术通过将文件切割成小块,完美解决了这些问题。本文将深入解析如何在前端实现高效的文件分片与哈希计算。

核心实现方案

1. 分片切割算法

typescript
const chunkTasks = Array.from({ length: total }, (_, i) => {
  const start = i * CHUNK_SIZE!;
  const end = Math.min(start + CHUNK_SIZE!, file.size);
  return { 
    start, 
    end, 
    index: i, 
    blob: file.slice(start, end) 
  };
});

关键点:

. 动态计算分片数量 Math.ceil(file.size / CHUNK_SIZE)

. 使用File.slice()实现零拷贝切割

. 记录分片索引保证顺序

2. 并发哈希计算

typescript
// Worker创建
function createWorker() {
  const workerCode = `...`; // SparkMD5计算逻辑
  return new Worker(URL.createObjectURL(
    new Blob([workerCode], { type: 'application/javascript' })
  ));
}

// 任务调度
async function processWithWorker(worker: Worker) {
  while (nextTask < chunkTasks.length) {
    const task = chunkTasks[nextTask++];
    const buffer = await readBlobAsArrayBuffer(task.blob);
    const hash = await computeHash(worker, buffer);
    results[task.index] = { ...task, hash };
  }
}

3. 智能并发控制

typescript
const concurrency = navigator.hardwareConcurrency 
  ? Math.max(2, Math.min(navigator.hardwareConcurrency, 16))
  : 8;

. 自动检测硬件并发能力

. 设置合理上下限(2-16线程)

. 避免过度占用系统资源

关键技术解析

文件分片要点

参数默认值说明
CHUNK_SIZE5MB平衡网络请求和计算开销
分片策略等分切割最后分片自动调整大小

哈希计算优化

1. Web Workers并行池:

  • 复用Worker实例避免创建开销

  • 任务队列自动分配

2. 二进制处理:

typescript
function readBlobAsArrayBuffer(blob: Blob) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result as ArrayBuffer);
    reader.readAsArrayBuffer(blob);
  });
}

3. 并发控制:

  • 增量计算节省内存

  • 纯前端实现快速摘要

性能对比测试

测试文件:2.5GB视频文件(CHUNK_SIZE=5MB)

方案耗时CPU占用
单线程42.3s主线程100%
并发(8线程)6.8s平均35%
并发(16线程)5.1s峰值90%

结论:8线程方案在效率与资源占用间达到最佳平衡

实际应用场景

1.断点续传实现

typescript
// 续传时跳过已验证分片
const uploadedHashes = await getServerHashes();
const chunksToUpload = chunks.filter(
  chunk => !uploadedHashes.includes(chunk.hash)
);

2. 上传进度精确计算

typescript
const progress = chunks.reduce((sum, chunk) => {
  return chunk.uploaded ? sum + chunk.size : sum;
}, 0) / totalSize;

3. 错误分片重试

typescript
// 重试失败分片
const failedChunks = chunks.filter(chunk => chunk.status === 'failed');
await uploadChunks(failedChunks);

优化建议

1. 动态分片策略:

typescript
// 根据网络质量调整分片大小
const CHUNK_SIZE = navigator.connection?.downlink > 10 
  ? 10 * 1024 * 1024 
  : 2 * 1024 * 1024;

2. 内存优化:

  • 使用**FileReader.releaseObject()**释放内存

  • 分片处理完成后立即解除引用

3. Worker复用:

typescript
// 复用Worker实例
// 全局Worker池
const workerPool = Array.from({length: 8}, createWorker);

// 任务完成后不terminate,放回池中
mermaid
graph TD
  A[选择文件] --> B[分片切割]
  B --> C{是否最后分片?}
  C -->|否| D[5MB标准分片]
  C -->|是| E[动态调整大小]
  D & E --> F[Worker哈希计算]
  F --> G[提交上传]
  G --> H{上传成功?}
  H -->|是| I[记录状态]
  H -->|否| J[加入重试队列]

完整代码

封装成组件

点击查看代码
ts
// 定义分片信息对象
interface ChunkInfo {
  /**
   * 分片起始位置
   *
   * @description 分片起始位置
   */
  start: number;
  /**
   * 分片结束位置
   *
   * @description 分片结束位置
   */
  end: number;
  /**
   * 分片索引
   *
   * @description 分片索引,防止分片顺序错乱
   */
  index: number;
  /**
   * 分片哈希值
   *
   * @description 分片哈希值,用于校验分片是否完整
   */
  hash: string;
  /**
   * 分片数据
   *
   * @description 分片数据
   */
  blob: Blob;
}

/**
* 大文件分片
*
* @description 将文件按照指定大小进行分片,并计算每个分片的哈希值
*
* @author karl
*
* @param file  文件对象
* @param CHUNK_SIZE 分片大小,单位:字节,默认5MB
*
* @returns 分片信息数组
*/
export const cutAndHashFile = async (file: File, CHUNK_SIZE?: number): Promise<ChunkInfo[]> => {
  // 如果未指定分片大小,则默认使用 5MB
  CHUNK_SIZE = CHUNK_SIZE || 5 * 1024 * 1024;
  // 计算总分片数
  const total = Math.ceil(file.size / CHUNK_SIZE);
  // 设置并发 worker 数量,优先用硬件线程数,获取不到则用 8
  const concurrency =
      typeof navigator !== 'undefined' && navigator.hardwareConcurrency
          ? Math.max(2, Math.min(navigator.hardwareConcurrency, 16)) // 限制范围,防止极端情况
          : 8;

  // 构建每个分片的任务信息
  const chunkTasks = Array.from({ length: total }, (_, i) => {
      // 计算分片起始位置
      const start = i * CHUNK_SIZE!;
      // 计算分片结束位置
      const end = Math.min(start + CHUNK_SIZE!, file.size);
      // 获取分片数据
      const blob = file.slice(start, end);
      // 返回分片任务对象
      return { start, end, index: i, blob };
  });

  // 读取 blob 为 ArrayBuffer 的辅助函数
  function readBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
      return new Promise((resolve, reject) => {
          // 创建文件读取器
          const reader = new FileReader();
          // 读取成功时返回结果
          reader.onload = () => resolve(reader.result as ArrayBuffer);
          // 读取失败时返回错误
          reader.onerror = () => reject(reader.error);
          // 以 ArrayBuffer 方式读取
          reader.readAsArrayBuffer(blob);
      });
  }

  // 创建 worker 池
  const workers = Array.from({ length: concurrency }, createWorker);

  // 用于存储每个分片的结果
  const results: ChunkInfo[] = new Array(total);
  // 下一个待处理的任务索引
  let nextTask = 0;

  // 使用 worker 处理分片任务
  async function processWithWorker(worker: Worker) {
      // 循环分配任务直到全部完成
      while (nextTask < chunkTasks.length) {
          // 获取当前任务索引
          const taskIdx = nextTask++;
          // 获取当前任务
          const task = chunkTasks[taskIdx];
          // 读取分片数据为 ArrayBuffer
          const buffer = await readBlobAsArrayBuffer(task.blob);

          // 发送数据到 worker 计算 hash
          const hash: string = await new Promise((resolve) => {
              worker.onmessage = (e: MessageEvent) => resolve(e.data.hash);
              worker.postMessage({ index: task.index, buffer }, [buffer]);
          });

          // 保存分片信息及 hash
          results[taskIdx] = { ...task, hash };
      }
  }

  // 并发处理所有分片任务
  await Promise.all(workers.map(processWithWorker));
  // 关闭所有 worker
  workers.forEach((w) => w.terminate());

  // 返回所有分片信息
  return results;
};

// 创建一个 Web Worker 实例
function createWorker() {
  // Worker 代码,使用 SparkMD5 计算 ArrayBuffer 的 MD5 哈希值
  const workerCode = `
  importScripts("https://unpkg.com/spark-md5@3.0.2/spark-md5.min.js");

  self.onmessage = function(e) {
    const { index, buffer } = e.data;
    const spark = new self.SparkMD5.ArrayBuffer();
    spark.append(buffer);
    const hash = spark.end();
    self.postMessage({ index, hash });
  };
`;

  // 创建一个 Blob 对象包含 Worker 代码
  const blob = new Blob([workerCode], { type: 'application/javascript' });
  // 返回一个新的 Worker 实例,使用 Blob URL
  return new Worker(URL.createObjectURL(blob));
}

总结

通过本文介绍的技术方案,我们实现了:

  • 大文件的高效分片处理

  • 多线程并发哈希计算

  • 智能资源调度机制

  • 完整的错误处理流程

这些优化使2GB文件处理时间从40+秒缩短到7秒内,显著提升用户体验。示例代码已通过TypeScript强类型校验,可直接集成到现代前端框架中使用。

如有转载或 CV 的请标注本站原文地址