在做视频上传系统时,产品经理提了个需求:
"所有上传的
.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 卡死
我们有个视频素材管理系统,要求:
- 用户上传
.mov
视频文件 - 前端计算文件的 SHA-256 Hash
- 检查服务端是否已存在相同文件
- 避免重复上传
但大文件的 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、
window
、document
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 | 极致性能 | 学习成本高 | 视频编码、图像处理 |
六、举一反三:三个变体场景实现思路
- 需要计算多个文件的 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 }))
}
}
}
-
实现断点续算(大文件上传)
计算每块的 Hash,最后合并,支持暂停恢复。
-
结合 Service Worker 预计算
在用户选择文件后,后台静默计算 Hash,提升上传体验。
总结
Web Worker 不是"高级技巧",而是处理 CPU 密集型任务的必备工具。
记住这个口诀:
耗时任务移出主线程,
消息通信是唯一通道,
分块读取防内存爆,
用完即毁别忘了。
当你在处理:
- 大文件 Hash 计算
- 图片/视频压缩
- 数据加密解密
- 复杂 JSON 解析
先想 Web Worker,再想其他方案。
它可能增加一点复杂度,但换来的是流畅的用户体验和专业的工程素养。
下次再有人问你"怎么避免页面卡死",别再说"加个 loading"了------直接甩出这行代码:
js
const worker = new Worker('/task.js')
worker.postMessage(data)