用 Web Worker 计算大视频文件 Hash:从“页面卡死”到流畅上传

在做视频上传系统时,产品经理提了个需求:

"所有上传的 .mov 视频文件,必须先计算 SHA-256 Hash,防止重复上传。"

我心想:"简单",于是写了段同步代码:

js 复制代码
// ❌ 危险!不要这样写
function calcHash(file) {
  const reader = new FileReaderSync()
  const arrayBuffer = reader.readAsArrayBuffer(file)
  const hash = crypto.subtle.digest('SHA-256', arrayBuffer)
  return hash
}

结果测试时,一个 1GB 的 .mov 文件直接让页面卡了 30 秒,用户根本没法操作。

这就是典型的 主线程阻塞 问题。今天我们用 Web Worker 来解决它。


一、问题场景:大文件 Hash 计算导致 UI 卡死

我们有个视频素材管理系统,要求:

  1. 用户上传 .mov 视频文件
  2. 前端计算文件的 SHA-256 Hash
  3. 检查服务端是否已存在相同文件
  4. 避免重复上传

但大文件的 Hash 计算是 CPU 密集型操作,会阻塞主线程,导致:

  • 页面无响应
  • 动画卡顿
  • 用户以为浏览器崩溃

二、解决方案:用 Web Worker 异步计算 Hash

我们把计算逻辑移到 Web Worker 中:

js 复制代码
// main.js(主线程)
async function calcHash(file) {
  return new Promise((resolve, reject) => {
    // 创建 Worker
    const worker = new Worker('/hash-worker.js', { type: 'module' })

    // 监听结果
    worker.onmessage = (e) => {
      if (e.data.error) {
        reject(new Error(e.data.error))
      } else {
        resolve(e.data.hash)
      }
      worker.terminate() // 🔍 用完即毁
    }

    worker.onerror = (err) => {
      reject(err)
      worker.terminate()
    }

    // 发送文件给 Worker
    worker.postMessage({ file }, [file]) // 🔐 Transferable 优化
  })
}

// 使用
document.getElementById('fileInput').addEventListener('change', async (e) => {
  const file = e.target.files[0]
  if (file && file.name.endsWith('.mov')) {
    try {
      const hash = await calcHash(file)
      console.log('File Hash:', hash)
      // 继续上传逻辑...
    } catch (err) {
      console.error('Hash 计算失败:', err)
    }
  }
})
js 复制代码
// hash-worker.js(Worker 线程)
self.onmessage = async function(e) {
  const { file } = e.data
  
  try {
    // 🔍 分块读取,避免内存溢出
    const chunkSize = 1024 * 1024 // 1MB 每块
    const hasher = new Hasher('SHA-256')
    
    let offset = 0
    while (offset < file.size) {
      const chunk = file.slice(offset, offset + chunkSize)
      const arrayBuffer = await chunk.arrayBuffer()
      hasher.update(arrayBuffer)
      offset += chunkSize
      
      // 🔐 定期通知主线程进度
      if (offset % (chunkSize * 10) === 0) {
        self.postMessage({ 
          progress: Math.round((offset / file.size) * 100) 
        })
      }
    }
    
    const hash = await hasher.digest()
    self.postMessage({ hash })
  } catch (err) {
    self.postMessage({ error: err.message })
  }
}

// 简易 Hasher 类(实际可用 crypto.subtle)
class Hasher {
  constructor(algorithm) {
    this.algorithm = algorithm
    this.chunks = []
  }
  
  update(chunk) {
    this.chunks.push(chunk)
  }
  
  async digest() {
    const combined = this.concatArrays(this.chunks)
    const hashBuffer = await crypto.subtle.digest(this.algorithm, combined)
    return this.bufferToHex(hashBuffer)
  }
  
  concatArrays(arrays) {
    const totalLength = arrays.reduce((acc, arr) => acc + arr.byteLength, 0)
    const result = new Uint8Array(totalLength)
    let offset = 0
    arrays.forEach(arr => {
      result.set(new Uint8Array(arr), offset)
      offset += arr.byteLength
    })
    return result
  }
  
  bufferToHex(buffer) {
    return Array.from(new Uint8Array(buffer))
      .map(b => b.toString(16).padStart(2, '0'))
      .join('')
  }
}

三、原理剖析:从表面到浏览器线程模型的三层机制

1. 表面用法:Web Worker 是什么?

Web Worker 是运行在后台的 JavaScript 线程,独立于主线程,不会阻塞 UI。

我们来画一张 主线程与 Worker 线程通信图

scss 复制代码
[主线程]                    [Worker 线程]
   |                               |
   |--- postMessage(data) -------->|
   |                               |
   |                               | → 执行耗时计算
   |                               |
   |<-- onmessage(result) ---------|
   |                               |
  • 通信必须通过 postMessage / onmessage
  • 数据传递是复制转移(Transferable)
  • 无法访问 DOM、windowdocument

2. 底层机制:浏览器如何调度 Worker?

当创建 Web Worker 时:

markdown 复制代码
1. 浏览器创建新线程(或从线程池分配)
2. 加载并执行指定脚本
3. 主线程与 Worker 通过消息队列通信
4. V8 引擎为每个线程维护独立的堆内存
5. 垃圾回收独立进行
6. GPU/CPU 资源由操作系统调度

关键优化点:

  • Transferable Objects :如 ArrayBuffer,可以用 postMessage(data, [data]) 转移所有权,避免复制
  • SharedArrayBuffer:多个线程共享内存(需 HTTPS + COOP/COEP)
  • Worker Pool:复用 Worker 实例,避免频繁创建销毁

3. 设计哲学:为什么需要 Web Worker?

问题 Web Worker 解决方案
主线程阻塞 耗时任务移出主线程
UI 卡顿 保持 60fps 流畅渲染
大文件处理 分块计算 + 进度反馈
并行计算 多 Worker 处理不同任务

💡 类比:

Web Worker 就像"后台厨房"------

  • 前台(主线程)负责接待顾客(UI 交互)
  • 厨房(Worker)负责做菜(计算任务)
  • 服务员(MessageChannel)传递订单和菜品

四、实战优化:处理超大 .mov 文件

1. 分块读取避免内存溢出

js 复制代码
async function* readInChunks(file, chunkSize = 1024 * 1024) {
  let offset = 0
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize)
    yield await chunk.arrayBuffer()
    offset += chunkSize
  }
}

// Worker 中使用
for await (const chunk of readInChunks(file)) {
  hasher.update(chunk)
}

2. 添加进度反馈

js 复制代码
// Worker
self.postMessage({ progress: 50 })

// 主线程
worker.onmessage = (e) => {
  if (e.data.progress !== undefined) {
    updateProgressBar(e.data.progress)
  }
}

3. 错误处理与超时

js 复制代码
const worker = new Worker('/hash-worker.js')
const timeout = setTimeout(() => {
  worker.terminate()
  reject(new Error('Hash 计算超时'))
}, 60000) // 60秒超时

worker.onmessage = (e) => {
  clearTimeout(timeout)
  // 处理结果
}

五、对比主流方案

方案 优点 缺点 适用场景
Web Worker 真实多线程,不阻塞 UI 通信成本高,不能访问 DOM 大文件计算、加密、解析
setTimeout 分片 简单,兼容性好 仍是单线程,总耗时不变 小任务分片
requestIdleCallback 利用空闲时间 执行时机不确定 低优先级任务
WebAssembly + Worker 极致性能 学习成本高 视频编码、图像处理

六、举一反三:三个变体场景实现思路

  1. 需要计算多个文件的 Hash
    创建 Worker 池,并发处理多个文件。
js 复制代码
class WorkerPool {
  constructor(size, script) {
    this.workers = Array(size).fill().map(() => new Worker(script))
    this.queue = []
  }
  
  exec(task) {
    const worker = this.workers.find(w => !w.busy)
    if (worker) {
      worker.busy = true
      worker.postMessage(task)
      return worker
    } else {
      return new Promise(resolve => this.queue.push({ task, resolve }))
    }
  }
}
  1. 实现断点续算(大文件上传)

    计算每块的 Hash,最后合并,支持暂停恢复。

  2. 结合 Service Worker 预计算

    在用户选择文件后,后台静默计算 Hash,提升上传体验。


总结

Web Worker 不是"高级技巧",而是处理 CPU 密集型任务的必备工具

记住这个口诀:

耗时任务移出主线程,
消息通信是唯一通道,
分块读取防内存爆,
用完即毁别忘了。

当你在处理:

  • 大文件 Hash 计算
  • 图片/视频压缩
  • 数据加密解密
  • 复杂 JSON 解析

先想 Web Worker,再想其他方案

它可能增加一点复杂度,但换来的是流畅的用户体验和专业的工程素养

下次再有人问你"怎么避免页面卡死",别再说"加个 loading"了------直接甩出这行代码:

js 复制代码
const worker = new Worker('/task.js')
worker.postMessage(data)
相关推荐
求知若渴,虚心若愚。4 分钟前
Error reading config file (/home/ansible.cfg): ‘ACTION_WARNINGS(default) = True
linux·前端·ansible
LinDaiuuj1 小时前
最新的前端技术和趋势(2025)
前端
一只小风华~1 小时前
JavaScript 函数
开发语言·前端·javascript·ecmascript·web
程序猿阿伟2 小时前
《不只是接口:GraphQL与RESTful的本质差异》
前端·restful·graphql
仰望星空的凡人3 小时前
【JS逆向基础】数据库之MongoDB
javascript·数据库·python·mongodb
若梦plus3 小时前
Nuxt.js基础与进阶
前端·vue.js
樱花开了几轉4 小时前
React中为甚么强调props的不可变性
前端·javascript·react.js
风清云淡_A4 小时前
【REACT18.x】CRA+TS+ANTD5.X实现useImperativeHandle让父组件修改子组件的数据
前端·react.js
小飞大王6664 小时前
React与Rudex的合奏
前端·react.js·前端框架
若梦plus4 小时前
React之react-dom中的dom-server与dom-client
前端·react.js