前端开发中有一个经典的性能瓶颈:UI 卡顿。无论你的 React 或 Vue 优化得再好,只要在主线程里执行了计算量巨大的循环,页面就可能陷入卡死的状态。
在 B 端业务中,Excel 导出、大图压缩、MD5 计算,都是典型的“主线程杀手”。面试时也常被问到:“如何保证在进行大量计算时,页面依然能流畅滚动?”
答案就是 Web Worker。它允许我们在主线程之外,开辟一个新的线程来执行耗时任务。今天,我们就通过两个真实场景,来彻底掌握这项多线程前端开发技术。
01. Web Worker 基础:主仆对话
Web Worker 的核心逻辑很简单:你可以把主线程想象成老板,把 Worker 线程看作助理。它们之间不共享内存(大部分情况下),只能通过消息传递 (postMessage) 来沟通。
Main.js (老板):
const worker = new Worker(new URL('./worker.js', import.meta.url));
// 1. 给助理派活
worker.postMessage({ type: ‘start‘, payload: 10000 });
// 2. 听助理汇报
worker.onmessage = (e) => {
console.log(‘助理干完了,结果是:‘, e.data);
};
Worker.js (助理):
self.onmessage = (e) => {
const { payload } = e.data;
// 3. 干脏活累活 (耗时计算)
const result = heavyComputation(payload);
// 4. 交差
self.postMessage(result);
};
02. 实战场景 A:10 万条数据导出 Excel
使用 SheetJS (xlsx) 库导出 Excel 时,将大量数据生成为二进制文件是一个非常耗时的过程,直接在前端框架/工程化的主线程中操作会导致页面无响应。
❌ 错误写法(在主线程执行):
import * as XLSX from 'xlsx';
function exportExcel(data) {
// 这行代码执行期间,页面是动不了的!
const workBook = XLSX.utils.json_to_sheet(data);
const blob = XLSX.write(workBook, { type: 'array' });
saveAs(new Blob([blob]), ‘data.xlsx‘);
}
✅ 正确写法(在 Worker 线程执行):
excel.worker.js:
import * as XLSX from ‘xlsx‘;
self.onmessage = (e) => {
const { data } = e.data;
// 耗时操作在这里跑,完全不影响主线程
const workSheet = XLSX.utils.json_to_sheet(data);
const workBook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workBook, workSheet, “Sheet1“);
// 生成二进制流
const arrayBuffer = XLSX.write(workBook, { bookType: ‘xlsx‘, type: ‘array‘ });
// 💡 高级技巧:使用 Transferable Objects 零拷贝传输(性能极致优化)
self.postMessage({ buffer: arrayBuffer }, [arrayBuffer]);
};
主页面调用:
const worker = new Worker(new URL(‘./excel.worker.js‘, import.meta.url));
worker.onmessage = (e) => {
const { buffer } = e.data;
const blob = new Blob([buffer]);
// 只有下载这步在主线程,瞬间完成
const url = URL.createObjectURL(blob);
const a = document.createElement(‘a‘);
a.href = url;
a.download = ‘report.xlsx‘;
a.click();
};
// 发送 10万条数据
worker.postMessage({ data: bigDataList });
03. 实战场景 B:图片压缩 (OffscreenCanvas)
过去,Web Worker 无法操作 DOM,因此处理图片压缩等图形任务很困难。但现在,所有主流浏览器都已支持 OffscreenCanvas(离屏 Canvas),这意味着 Worker 线程也能处理图形了!
image.worker.js:
self.onmessage = async (e) => {
const { file, quality } = e.data;
// 1. 将 File 转为 ImageBitmap (Worker 支持的图片格式)
const bitmap = await createImageBitmap(file);
// 2. 创建离屏 Canvas
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext(‘2d‘);
// 3. 绘制并压缩
ctx.drawImage(bitmap, 0, 0);
const compressedBlob = await canvas.convertToBlob({
type: ‘image/jpeg‘,
quality: quality || 0.8
});
self.postMessage({ blob: compressedBlob });
};
在主页面中,用户选择图片后,直接将 File 对象传递给 Worker。图片在 Worker 中完成压缩后,再将结果返回给主线程进行上传等操作,整个过程对主线程的流畅度毫无影响。
04. 工程化封装:Comlink
原生的 postMessage API 写起来像是在处理事件监听,代码结构可能不够清晰。Google 推出的 Comlink 库,可以让 Web Worker 的使用体验变得像调用本地函数一样丝滑。
Worker 端:
import * as Comlink from ‘comlink‘;
const exports = {
heavySum(a, b) { return a + b; }, // 假设这是耗时任务
compressImage(file) { … }
};
Comlink.expose(exports);
主线程端:
import * as Comlink from ‘comlink‘;
const worker = new Worker(...);
// 将 worker 包装成一个 Promise 化的对象
const api = Comlink.wrap(worker);
// 像调用普通异步函数一样调用 Worker 中的方法!
const result = await api.heavySum(1, 2);
结语
Web Worker 是前端性能优化的一件“核武器”,它从根本上将“UI 渲染”和“逻辑计算”进行了物理隔离。随着 WebAssembly 和 OffscreenCanvas 等技术的普及,未来将有更多重型任务(如视频剪辑、3D 建模、大文件处理)可以迁移到 Worker 线程中执行。
别让用户再面对卡死的页面发呆,是时候为你的前端应用招募一位高效的“计算助理”了。如果你想了解更多前端性能优化技巧或交流实战经验,欢迎来 云栈社区 与更多开发者一起探讨。