首先:我为什么会想起来写这样一篇文章,因为本人开发了一个npm库enlarge-file-upload 前端大文件上传库,专门处理大文件上传的问题,其中就需要使用到hash计算,所以这里就自己的一些心得体会,写下这篇文章。
在前端领域中,大文件的哈希计算一直是一项棘手的任务。文件越大,哈希计算越耗时,用户等待越久,体验越差。为此出现了多种优化方案,例如抽样哈希、增量哈希、并行分片等等。
本文重点介绍一种在实际项目中非常常见也非常有效的优化方式:使用 Web Worker 进行多线程并行计算。下面将从基础概念开始,逐步解析 Web Worker 如何用于哈希计算、能够解决什么问题、是否能让哈希更快,以及它所带来的限制。
🧩 一、什么是 Web Worker?
前端 JavaScript 的运行环境长期是单线程模型 :
➡️ UI 渲染
➡️ 用户交互事件
➡️ 网络请求回调
➡️ 业务逻辑计算
全部都在同一个主线程执行。
大量计算任务(例如 1GB 文件的 SHA256)会让主线程阻塞,使得:
❌ 页面卡顿
❌ UI 停滞
❌ 滑动、点击无响应
为了解决这一问题,Web Worker 作为 HTML5 标准的一部分在 2009 年左右由 WHATWG / W3C 提出并规范化,浏览器随后陆续实现。
它允许:
✔ 在独立线程中运行 JavaScript
✔ 与主线程通过消息机制通信
✔ 不阻塞 UI,不影响交互
因此,Web Worker 成为前端执行大计算任务(包括哈希计算)的重要手段。
🚀 二、Web Worker 在哈希计算中到底解决了什么问题?
🟦 Web Worker 能解决的核心问题:
✔ 1. 不阻塞 UI
即使文件很大、任务很重,用户仍能流畅交互。
✔ 2. I/O 与 CPU 可以并行
文件的读取(I/O)与哈希计算(CPU)可以分布到多个 Worker 中并行处理。
✔ 3. 更好的浏览器资源利用
现代浏览器通常有 4~16 核心 CPU,Web Worker 让前端可以利用多核资源。
❓ Web Worker 会让哈希计算更快吗?
答案是:
▶ 既可以让它更快,也可能不会更快
取决于你使用哪一种方案。
下面我们讨论两种最典型的方案。
🥇 方案一:并行计算分片哈希(Chunked Hashing)
思路:每个 Worker 自己计算分片的 hash,主线程再把这些 hash 合并生成最终结果。
流程如下:
Worker(每个线程):
- 读取自己的分片
- 对该分片做 SHA256
- 返回 hash 结果
主线程:
- 按顺序把所有 chunk 的 hash 拼接
- 对拼接后的字符串再次做一次 SHA256
最终得到一个"分段哈希值"(不是标准 SHA256)。
📌 示例代码(方案一)
(代码与你提供的版本一致,仅做格式整理)
ini
import sha256 from "./sha256.js";
async function computeFileHashParallel(file, options = {}) {
const {
chunkSize = 5 * 1024 * 1024,
workerCount = 4,
onProgress = null
} = options;
const fileSize = file.size;
const chunkCount = Math.ceil(fileSize / chunkSize);
console.log(`大小: ${fileSize}, 分片: ${chunkCount}, Worker: ${workerCount}`);
// Worker 脚本
const workerCode = `
self.importScripts("sha256.js");
self.onmessage = async (e) => {
const { file, startChunk, endChunk, chunkSize } = e.data;
const chunkHashes = [];
for (let i = startChunk; i < endChunk; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
const buffer = await blob.arrayBuffer();
// 对单个 chunk 计算独立 hash
const h = sha256(buffer);
chunkHashes.push({ index: i, hash: h });
}
self.postMessage({ chunkHashes });
};
`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const workerUrl = URL.createObjectURL(blob);
const workers = Array.from({ length: workerCount }, () => new Worker(workerUrl));
const chunksPerWorker = Math.ceil(chunkCount / workerCount);
let completedWorkers = 0;
const workerResults = [];
const promises = workers.map((worker, index) => {
return new Promise((resolve) => {
const startChunk = index * chunksPerWorker;
const endChunk = Math.min(startChunk + chunksPerWorker, chunkCount);
if (startChunk >= chunkCount) {
resolve();
return;
}
worker.onmessage = (e) => {
workerResults[index] = e.data.chunkHashes;
completedWorkers++;
if (onProgress) {
onProgress({
stage: "hashing",
completed: completedWorkers,
total: workerCount,
percentage: ((completedWorkers / workerCount) * 100).toFixed(2)
});
}
resolve();
};
worker.postMessage({
file,
startChunk,
endChunk,
chunkSize
});
});
});
await Promise.all(promises);
// 清理
workers.forEach(w => w.terminate());
URL.revokeObjectURL(workerUrl);
console.log("所有分片已并行哈希,开始合并最终哈希");
// 主线程合并哈希
const sha = sha256.create();
// 按 index 排序,保证顺序正确
const allChunks = workerResults.flat().sort((a, b) => a.index - b.index);
for (const item of allChunks) {
sha.update(item.hash);
}
return sha.hex();
}
🧠 原理简述
每个 Worker:
erlang
chunk0 → hash0
chunk1 → hash1
...
主线程:
ini
final = sha256(hash0 + hash1 + hash2 + ...)
⚡ 最大优势:速度巨快
利用多核 CPU,速度提升可达 3~4 倍,尤其对几 GB 的超大文件效果拔群。
❗ 最大缺点:结果不是标准 SHA256
这种方法计算出来的 hash ≠ 原始文件的完整 SHA256。
🥈 方案二:Worker 只负责读取分片,主线程负责真正的哈希合并
为了得到 标准的 SHA256,必须保证:
📌 所有数据按照文件原始顺序流入 sha.update()
📌 不能改变哈希算法本身的输入结构
因此,第二种方案让 Worker 不计算 hash,只负责读取分片。
📌 示例代码(方案二)
(与你原始代码一致,仅整理表达)
ini
import sha256 from "./sha256.js";
async function computeFileHashParallelFastOrdered(file, options = {}) {
const {
chunkSize = 8 * 1024 * 1024,
workerCount = 4,
onProgress = null,
} = options;
const fileSize = file.size;
const chunkCount = Math.ceil(fileSize / chunkSize);
// Worker 代码:只读取 + 返回 { index, buffer }
const workerCode = `
self.onmessage = async (e) => {
const { file, index, start, end } = e.data;
const blob = file.slice(start, end);
const buffer = await blob.arrayBuffer();
self.postMessage({ index, buffer }, [buffer]);
};
`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const workerUrl = URL.createObjectURL(blob);
const workers = Array.from({ length: workerCount }, () => new Worker(workerUrl));
const sha = sha256.create();
let nextNeededIndex = 0; // 下一个必须按顺序 update 的分片
const received = {}; // 缓存乱序到达的分片
let completed = 0;
function tryAppendChunks() {
while (received[nextNeededIndex]) {
const buffer = received[nextNeededIndex];
sha.update(new Uint8Array(buffer));
delete received[nextNeededIndex];
nextNeededIndex++;
if (onProgress) {
onProgress({
completed: nextNeededIndex,
total: chunkCount,
percentage: ((nextNeededIndex / chunkCount) * 100).toFixed(2),
});
}
}
}
// 分派任务
const tasks = [];
for (let i = 0; i < chunkCount; i++) {
tasks.push(i);
}
function assignWork(worker) {
return new Promise((resolve) => {
if (tasks.length === 0) return resolve();
const index = tasks.shift();
const start = index * chunkSize;
const end = Math.min(start + chunkSize, fileSize);
worker.onmessage = (e) => {
const { index, buffer } = e.data;
// 存入缓存
received[index] = buffer;
// 尝试按顺序加入 sha.update()
tryAppendChunks();
completed++;
if (completed >= chunkCount) resolve();
else resolve(assignWork(worker));
};
worker.postMessage({ file, index, start, end });
});
}
await Promise.all(workers.map(w => assignWork(w)));
workers.forEach(w => w.terminate());
URL.revokeObjectURL(workerUrl);
return sha.hex();
}
🧠 原理简述
每个 Worker:
读取 chunk → 返回 buffer(二进制数据)
例如:
erlang
chunk0 → buffer0
chunk1 → buffer1
chunk2 → buffer2
...
主线程:
严格按顺序执行:
scss
sha.update(buffer0)
sha.update(buffer1)
sha.update(buffer2)
...
final = sha.hex()
⚡ 最大优势:得到标准、正确的 SHA256
与"直接对整个文件做 SHA256"完全一致,可用于秒传、断点续传校验、数据完整性验证等场景。
❗ 最大缺点:速度提升有限
只有 I/O(读取分片)是并行的;
哈希计算仍然必须在主线程按顺序串行执行,因此无法像方案一那样快。
📊 对比两种方案
| 特点 | 方案一:分片哈希(chunk hash) | 方案二:原始顺序哈希 |
|---|---|---|
| 是否标准 SHA256 | ❌ 否 | ✔ 是 |
| 速度 | ⭐⭐⭐⭐ 最高 | ⭐⭐ 中等 |
| Worker 执行内容 | 读取 + 计算哈希 | 仅读取 |
| 主线程执行内容 | 拼接 + 再次哈希 | 真正的完整哈希计算 |
| 是否适合秒传 / 去重 | ❌ 不可用 | ✔ 可用于所有标准应用 |
| 是否适合快速校验 | ✔ 可用 | ✔ 可用 |
🎯 总结
📌 Web Worker 的作用:
- 让大文件计算不阻塞 UI
- 让任务并行化,充分利用多核 CPU
- 可以显著提升哈希计算速度
📌 是否加速?取决于你的方案
| 想要 | 选择方案 |
|---|---|
| 快 3~4 倍速度,但不需要标准 SHA256 | 方案一 |
| 必须得到真正的 SHA256(如秒传、断点续传验证) | 方案二 |
如果你的需求是:
既要快,又要标准 SHA256
👉 只能选择 方案二,因为 SHA256 算法本质上是严格顺序的,无法被真正并行化。
前端大文件上传库:www.npmjs.com/package/enl... 感兴趣可以使用,有在线使用文档及演示案例