分片计算文件MD5,并借助WebWorker和队列优化

之前也有做过计算文件MD5做相关校验,最近业务上再次遇上,同时需要大量文件同时计算,难免需要优化相关性能。这里用到分片计算,同时将计算过程放到webworker,避免长时间占用主线程造成卡顿,并且通过队列控制并发,避免占用过多计算机资源,造成浏览器卡顿。

整个文件直接计算MD5

javascript 复制代码
import SparkMD5 from 'spark-md5'
const spark = new SparkMD5.ArrayBuffer() 
export const createMD5 = file => {
    // 验证file
    return new Promise((resolve,reject)=>{
        const fileReader = new FileReader()
         fileReader.readAsArrayBuffer(file)
         fileReader.onerror = e => {
             console.error(e)
             return reject(e)
             }
        fileReader.onload = () => {
            try{
            resolve(spark.hash(dataBuffer)) 
            }catch(e){
                 console.error(e)
             return reject(e)
            }
            
         }
    })
    
}

直接加载整个文件,将整个文件的二进制数据arrayBuffer丢给计算工具计算MD5,如果是一个小文件就还好,但是大文件,批量的文件,一下子将大量资源放到内存里会造成明显的卡顿 .

分片计算MD5

spark-md5推荐:Incremental md5 performs a lot better for hashing large amounts of data, such as files. One could read files in chunks, using the FileReader & Blob's, and append each chunk for md5 hashing while keeping memory usage low.

增量 md5在散列大量数据(比如文件)时表现得更好。可以使用 FileReader & Blob 的块来读取文件,并为 md5哈希添加每个块,同时保持较低的内存使用。

通过File.slice将文件分片,一块一块地增量处理spark.append(chunkArrayBuffer),最后通过spark.end()获取最终的文件md5,可以保持低内存使用,当然,我们平时讲的文件分片上传等也是基于File.slice。

上代码

javascript 复制代码
import SparkMD5 from 'spark-md5'
/*
 * @description 创建文件的MD5值,根据文件大小动态选择分片大小和快速循环的方式
 * @param {Object} file el文件对象或文件对象
 * @return {String} MD5 值
 * */
const maxChunkSize = 1024 * 1024 // 每次处理数据块的最大值,1MB
const chunksPerCycle = 100 // 每个计算周期中处理的数据块数
export const createMD5 = file => {
  file = file.raw || file
  let stamp = Date.now()
  console.time('md计算耗时' + stamp)
  return new Promise((resolve, reject) => {
    // 分片放worker处理
    const fileReader = new FileReader()
    try {
      let currentChunk = 0
      const totalChunks = Math.ceil(file.size / maxChunkSize)
      const spark = new SparkMD5.ArrayBuffer()
      // 处理数据块
      const processChunk = start => {
        try {
          const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
          const end = Math.min(start + maxChunkSize, file.size)
          const chunk = blobSlice.call(file, start, end)
          fileReader.readAsArrayBuffer(chunk)
        } catch (e) {
          console.error(e)
        }
      }
      // 文件读取成功
      fileReader.onload = () => {
       spark.append(fileReader.result) // 将当前数据块内容追加到 MD5 计算器
        currentChunk += 1
        if (currentChunk >= totalChunks) {
          const md5Hash = spark.end() // 完成 MD5 计算
           spark.destroy() // 销毁计算器
           fileReader.abort() //释放资源
           console.timeEnd('计算MD5')
           return resolve(md5Hash)
        } else if (currentChunk % chunksPerCycle === 0) {
          // 在处理指定数量的数据块后,设置一个任务延迟以使 UI 线程有空间处理
          setTimeout(() => {
            processChunk(currentChunk * maxChunkSize)
          }, 0)
        } else {
          // 继续处理下一个数据块
          processChunk(currentChunk * maxChunkSize)
        }
      }
      fileReader.onerror = e => {
        console.error(e)
        return reject(e)
      }
      // 开始处理第一个数据块
      processChunk(0)
    } catch (e) {
      fileReader.abort()
      reject(e)
    } finally {
    }
  })
}

借助WebWorker优化

md5计算属于高密度计算,如果直接放在js主线程,长时间占用造成卡顿难免对用户体验不好,这里也可以发挥出webworker的优势,本来webworker就是为了线程可以执行任务而不干扰用户界面而生。

这里也有两种方式,

第一种,将File直接传给worker,在worker里面分片读取计算,但是这样的话,因为File对象这种二进制数据直接传递给worker,浏览器会先给FIle做一层拷贝,但是拷贝方式多少会造成性能问题。

当然,二进制数据可以通过Transferable Objects,也就是转移数据的方法,主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了(防止出现多个线程同时修改数据的局面)。那么我们能不能将File通过这种方式传过去呢?不行,Transferable Objects只支持以下几个类型,就算支持将File转过去,我们还得想一个问题就是,将FIle转过去,主线程就不能用这个File对象了,那你后续怎么做进一步的处理,上传啥的。

以下是可以被转移的不同规范的对象:

第二种,反正也是切片,我这里就直接在主线程切片读取,将读取后的chunkBuffer通过Transferable Objects传递到worker线程。

worker代码:

php 复制代码
import SparkMD5 from 'spark-md5'
const spark = new SparkMD5.ArrayBuffer()
self.addEventListener('message', ({ data }) => {
  const { dataBuffer, status } = data
  try {
    if (status === 'ing') {
      spark.append(dataBuffer)
    } else if (status === 'end') {
      self.postMessage({ md5: spark.end(), status: 'success' })
      spark.destroy() // 销毁计算器
    }
  } catch (e) {
    self.postMessage({ status: 'error', error: e })
    console.error(e)
  }
})

主线程:

javascript 复制代码
/*
 * @description 创建文件的MD5值,根据文件大小动态选择分片大小和快速循环的方式
 * @param {Object} file el文件对象或文件对象
 * @return {String} MD5 值
 * */
const maxChunkSize = 1024 * 1024 // 每次处理数据块的最大值,1MB
const chunksPerCycle = 100 // 每个计算周期中处理的数据块数
export const createMD5 = file => {
  file = file.raw || file
  let stamp = Date.now()
  console.time('md计算耗时' + stamp)
  let worker = new Worker(
    /* webpackChunkName: "md5-encode.worker" */ new URL('../worker/md5-encode.worker.js', import.meta.url)
  )
  
  return new Promise((resolve, reject) => {
    // 分片放worker处理
    const fileReader = new FileReader()
    try {
      let currentChunk = 0
      const totalChunks = Math.ceil(file.size / maxChunkSize)
      // 处理数据块
      const processChunk = start => {
        try {
          let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
          const end = Math.min(start + maxChunkSize, file.size)
          const chunk = blobSlice.call(file, start, end)
          fileReader.readAsArrayBuffer(chunk)
        } catch (e) {
          console.error(e)
        }
      }
      // 文件读取成功
      fileReader.onload = () => {
        worker.postMessage({ dataBuffer: fileReader.result, status: 'ing' }, [fileReader.result])
        currentChunk += 1
        if (currentChunk >= totalChunks) {
          worker.postMessage({ status: 'end' })
          fileReader.abort()
          // console.timeEnd('计算MD5')
          return
          // return resolve(md5Hash)
        } else if (currentChunk % chunksPerCycle === 0) {
          // 在处理指定数量的数据块后,设置一个任务延迟以使 UI 线程有空间处理
          setTimeout(() => {
            processChunk(currentChunk * maxChunkSize)
          }, 0)
        } else {
          // 继续处理下一个数据块
          processChunk(currentChunk * maxChunkSize)
        }
      }
​
      fileReader.onerror = e => {
        console.error(e)
        return reject(e)
      }
       worker.onmessage = ({ data }) => {
        const { md5, status, error } = data
        if (status === 'success') {
          resolve(md5)
        } else {
          reject(error)
        }
        console.timeEnd('md计算耗时' + stamp)
        worker.terminate()
      }
      // 开始处理第一个数据块
      processChunk(0)
    } catch (e) {
      fileReader.abort()
      reject(e)
    } finally {
    }
  })
}

第二种方式地第一种直接传File对象会快出File拷贝的时间,处理一个300多m的文件,整体会快几百ms到1s。

借助队列函数进一步优化

现在计算md5不会堵塞主线程了,但是如果大量文件同时进行分片读取,同样会造成一定的卡顿,同时,大家也要知道,worker开到一定数量也是会造成浏览器卡顿的,和电脑的资源大小有关,所以可以通过队列控制计算md5方法的并发量,避免一下子占用太多资源。

以下是我写的一个队列函数,结合一下上面的createMD5即可达到目的。

javascript 复制代码
​
/**
 * 队列操作
 * @param concurrency 同时执行的数量
 * @param fn 操作函数 异步函数
 * @param fn.dataItem 操作的数据
 * @param fn.getRemoveQueue 撤销排队中的某一个任务
 * @returns {function(*=, *=): Promise<unknown>}
 * 使用方法
 * let removeFn = null
 * const getRemoveFn = fn => removeFn = fn
 * const handleDataByQueue = createQueue(3, async (dataItem) => {
 *  await sleep(1000)
 *  console.log(dataItem)
 *  return dataItem
 *  })
 *  handleDataByQueue(1, getRemoveFn)
 *  // 取消排队
 *  setTimeout(() => {
 *  removeFn && removeFn()
 *  }, 1000)
 * */
export const createQueue = (concurrency, fn) => {
  const queue = []
  const runningQueue = []
  // 撤销排队中的某一个任务
​
  const removeQueue = task => {
    const index = queue.findIndex(item => item === task)
    if (index !== -1) {
      console.log('取消排队')
      queue.splice(index, 1)
    }
  }
  const process = (dataItem, getRemoveQueueSource) => {
    return new Promise((resolve, reject) => {
      const run = async () => {
        if (runningQueue.length >= concurrency) {
          queue.push(run)
          getRemoveQueueSource && getRemoveQueueSource(() => removeQueue(run))
          return
        }
        runningQueue.push(run)
        try {
          const result = await fn(dataItem)
          resolve(result)
        } catch (e) {
          reject(e)
        } finally {
          runningQueue.splice(runningQueue.indexOf(run), 1)
          if (queue.length) {
            queue.shift()()
          }
        }
      }
      run()
    })
  }
  return process
}

使用:

scss 复制代码
// 10个并发量
const createMD5ByQueue = createQueue(10, createMD5)
​
// createMD5ByQueue(file1)
// createMD5ByQueue(file2)
// createMD5ByQueue(file3)
// createMD5ByQueue(file4)

如有不妥,多多指教!

相关推荐
tedcloud12334 分钟前
UI-TARS-desktop部署教程:构建AI桌面自动化系统
服务器·前端·人工智能·ui·自动化·github
UXbot4 小时前
AI原型设计工具如何支持团队协作与快速迭代
前端·交互·个人开发·ai编程·原型模式
ZC跨境爬虫4 小时前
跟着MDN学HTML_day_48:(Node接口)
前端·javascript·ui·html·音视频
PieroPc6 小时前
CAMWATCH — 局域网摄像头监控系统 Fastapi + html
前端·python·html·fastapi·监控
巴巴博一7 小时前
2026 最新:Trae / Cursor 一键接入 taste-skill 完整教程(让 AI 前端告别“AI 味”)
前端·ai·ai编程
kyriewen7 小时前
半夜三点线上崩了,AI替我背了锅——用AI排错,五分钟定位三年老bug
前端·javascript·ai编程
kyriewen7 小时前
我让 AI 当了 24 小时全年无休的“毒舌考官”
前端·ci/cd·ai编程
hexu_blog7 小时前
vue+java实现图片批量压缩
java·前端·vue.js
IT_陈寒8 小时前
为什么你应该学习JavaScript?
前端·人工智能·后端