文件MD5生成性能大提升!如何实现分片与Worker优化

在处理大文件上传时,生成文件的MD5哈希是一项重要的工作,可以用于文件校验、重复文件检查和数据完整性验证。然而,传统的MD5计算方法在处理大文件(如1GB以上的文件)时,往往效率较低。如何通过分片(chunking)技术和Web Worker实现MD5计算的显著性能提升。

我们通过这种优化方式,将1.67GB文件的MD5生成时间从18秒缩短到了仅3秒!

旧版实现:串行分片MD5计算

在原始的实现中,我们使用 FileReader 对文件进行分片处理,每次将分片转为ArrayBuffer,再通过SparkMD5库的增量计算功能逐片计算文件的MD5。虽然这种方法能够实现文件的逐片处理,但它仅使用单线程顺序计算,因此在大文件上会花费大量时间。以下是旧版实现的代码结构:

typescript 复制代码
function createHash(chunks: Blob[]) {
    return new Promise<string>(resolve => {
        const spark = new SparkMD5.ArrayBuffer()
        function _hash(index: number) {
            if (index >= chunks.length) {
                resolve(spark.end())
                return
            }
            const reader = new FileReader()
            reader.readAsArrayBuffer(chunks[index])
            reader.onload = e => {
                spark.append(e.target?.result as ArrayBuffer)
                _hash(index + 1)
            }
        }
        _hash(0)
    })
}

新版优化:利用Web Worker进行并行计算

新版实现的核心思路是通过Web Worker将分片处理和MD5计算过程并行化。具体来说,将文件划分为多个块,每个块通过Worker分发到独立的线程中进行处理,这样能够有效提高处理速度。

新版实现的关键步骤如下:

  1. 文件分块与分片:将文件划分为更大的块(例如50MB),并进一步拆分为小的分片(例如10MB),以适应Web Worker的处理。
  2. Worker管理与并发控制:通过线程池机制限制活跃Worker数量,避免性能瓶颈。
  3. 并行计算进度与总哈希合并:每个Worker计算完一个块的MD5后,将结果传回主线程,主线程合并所有块的哈希,得出最终的MD5值。

以下是新版优化中的关键代码:

typescript 复制代码
const worker = new Worker('./hashWorker.js')
worker.postMessage({ chunks: chunkArray })
worker.onmessage = event => {
    if (event.data.hash) {
        workerResolve(event.data.hash)
    } else if (event.data.progress) {
        progress.value += (event.data.progress / totalSize) * 100
    }
}

性能对比:3秒 vs 18秒

使用新版的优化方法,我们将1.67GB文件的MD5计算时间从18秒降低到了3秒,提升了6倍以上的速度。这种性能提升在处理大文件时尤为显著,为用户提供了更加流畅的文件上传体验。

整个流程如下

处理大文件的上传和哈希计算可能会面临性能瓶颈。以下是通过分块上传和 Web Worker 并发计算优化大文件 MD5 哈希值的完整步骤:

  1. 创建文件块和分片
  • 将大文件分割为若干个较大的块(如每块 50MB)。
  • 将每个块进一步拆分为更小的分片(如每片 10MB)。
  • createChunks 函数中创建块结构,在 createChunksFromBlock 函数中完成从块到分片的转换。
  1. 初始化 Worker
  • 使用线程池管理 Web Worker 并发数量,限制同时处理的 Worker 数量,避免性能压力。
  • 通过 postMessage 方法向 Worker 发送分片和块的总字节数。
  1. 在 Worker 中处理分片
  • 在 Worker 中接收分片数据,利用 FileReader 逐片读取内容并通过 SparkMD5 进行增量哈希计算。
  • 每处理完一个分片后,向主线程发送进度(已处理字节数)和中间哈希值。
  1. 计算总进度
  • 主线程根据每个块的处理情况更新整体进度。
  • 每当一个块的哈希计算完成,增加已处理块计数,并相应更新总进度。
  1. 合并哈希值
  • 所有块的哈希计算完成后,主线程收集每个块的哈希值。
  • 使用 SparkMD5 合并各块的哈希,最终生成整个文件的 MD5 值。
  1. 返回结果
  • 所有块的处理完成后,返回最终文件的 MD5 哈希值和总进度(通常为 100%)。

通过Web Worker与分片技术的结合,我们显著提升了大文件的MD5计算性能,这种方法不仅适用于MD5计算,也可用于其他大文件的处理场景。未来,我们可以进一步优化多线程逻辑,如增加断点续传、内存优化等,让文件处理更加高效!

完整代码

  1. hashWorker.js
js 复制代码
// hashWorker.js
// importScripts('https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js')
importScripts('/node_modules/spark-md5/spark-md5.min.js')

self.onmessage = function (event) {
  const chunks = event.data.chunks

  const spark = new SparkMD5.ArrayBuffer()
  let loaded = 0

  const readChunk = index => {
    if (index >= chunks.length) {
      self.postMessage({ hash: spark.end() })
      return
    }

    const fileReader = new FileReader()
    fileReader.onload = e => {
      spark.append(e.target.result)
      loaded++
      const chunkSize = chunks[index].size
      // 发送当前块已读字节数
      // 当前已读字节数
      const progress = loaded * chunkSize
      // 发送当前块的进度
      self.postMessage({ progress, size: chunkSize })
      readChunk(index + 1)
    }
    fileReader.onerror = () => {
      self.postMessage({ error: 'File reading error' })
    }
    fileReader.readAsArrayBuffer(chunks[index])
  }

  readChunk(0)
}
  1. useFileChunks
ts 复制代码
// useFileChunks.ts
import { ref } from 'vue'
import SparkMD5 from 'spark-md5'

/**
 *
 * 大文件分块上传和哈希
 * 1. 创建文件块和分片:
 * 1.1 将文件分成若干个较大的块(例如 50MB),每个块内部再分成更小的分片(例如 10MB)。
 * 1.2 在 createChunks 函数中实现块的创建,并在 createChunksFromBlock 函数中实现从块到分片的转换。
 *
 * 2. 初始化 Worker:
 * 2.1 在处理每个块时,使用线程池管理并发的 Web Worker,限制同时处理的 Worker 数量以避免性能问题。
 * 2.2 通过 postMessage 方法将分片和当前块的总字节数发送给 Worker。
 *
 * 3. 在 Worker 中处理分片:
 * 3.1 在 Worker 中接收分片数据,通过 FileReader 读取每个分片的内容,并使用 SparkMD5 计算每个分片的哈希值。
 * 3.2 每读取完一个分片,向主线程发送当前进度(已读取字节数)和最终的哈希值。
 *
 * 4. 计算总进度:
 * 4.1 在主线程中,根据每个块的处理进度,计算和更新总进度。
 * 4.2 每当一个块的哈希计算完成时,增加已处理的块计数,并根据已处理块数更新总进度。
 *
 * 5. 合并哈希值:
 *  在所有块的哈希计算完成后,收集每个块的哈希值,并使用 SparkMD5 合并这些哈希值,得到最终的文件哈希。
 *
 * 6. 返回结果:
 *  在所有处理完成后,返回文件的 MD5 哈希值和当前进度(可能达到 100%)。
 * @param blockSize 块大小 (默认 50MB)
 * @param chunkSize 分片大小 (默认 10MB)
 * @returns {md5: string, progress: number}
 */
export const useFileChunk = (
  blockSize = 50 * 1024 * 1024,
  chunkSize = 10 * 1024 * 1024,
) => {
  const md5 = ref<string>()
  const progress = ref<number>(0)
  const activeWorkers = ref<number>(0)

  // 设置最大 Worker 数量
  const MAX_WORKERS = 4

  // 创建块和分片
  function createChunks(file: File) {
    const blocks = []
    let cur = 0

    // 分块
    while (cur < file.size) {
      const block = file.slice(cur, cur + blockSize)
      blocks.push(block) // 保存块本身以计算字节数
      cur += blockSize
    }

    return blocks
  }

  // 从块中创建分片
  function createChunksFromBlock(block: Blob) {
    const result = []
    let cur = 0

    while (cur < block.size) {
      const chunk = block.slice(cur, cur + chunkSize)
      result.push(chunk)
      cur += chunkSize
    }

    return result
  }

  async function get(file: File) {
    if (!file) throw new Error('File is required')
    progress.value = 0
    md5.value = ''

    const blocks = createChunks(file) // 创建块和分片
    const totalBlocks = blocks.length // 计算块总数

    const workerPromises = blocks.map(block => {
      return new Promise((workerResolve, workerReject) => {
        const processBlock = () => {
          if (activeWorkers.value < MAX_WORKERS) {
            activeWorkers.value++
            const worker = new Worker(
              new URL('./hashWorker.js', import.meta.url),
            )
            const chunkArray = createChunksFromBlock(block)

            // 将块中的所有片发送给 Worker
            worker.postMessage({ chunks: chunkArray })

            worker.onmessage = event => {
              if (event.data.hash) {
                workerResolve(event.data.hash) // 返回当前块的哈希
                activeWorkers.value-- // 完成后减少活跃 Worker 数量
              } else if (event.data.size !== undefined) {
                // 更新块进度
                progress.value += (event.data.size / file.size) * 100
              } else if (event.data.error) {
                workerReject(event.data.error)
                activeWorkers.value-- // 出错时也减少活跃 Worker 数量
              }
            }

            worker.onerror = error => {
              workerReject('Worker error: ' + error.message)
              activeWorkers.value-- // 出错时减少活跃 Worker 数量
            }
          } else {
            // 如果当前活跃 Worker 已满,稍后重试
            setTimeout(processBlock, 100)
          }
        }

        processBlock() // 启动块处理
      })
    })

    // 等待所有块的哈希返回
    const hashes: string[] = (await Promise.all(
      workerPromises,
    )) as unknown as string[]

    // 合并哈希值
    const finalSpark = new SparkMD5()
    hashes.forEach(hash => finalSpark.append(hash))
    md5.value = finalSpark.end() // 计算最终的 MD5 值

    return { md5: md5.value, progress: progress.value, chunks: [] }
  }

  return { getFileInfo: get, progress }
}
相关推荐
Momo__42 分钟前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富1 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇1 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇1 小时前
React中的forwardRef
前端·react.js·面试
复杂网络1 小时前
Stable Diffusion 视觉大模型微调技术深度调研
算法
复杂网络1 小时前
基于 Stable Diffusion 架构的视觉大模型代表性工作与原理深度解析
算法
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
MrZhao4001 小时前
Agent Loop 如何用 Hook 扩展:权限、日志与工具拦截
算法
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端