JavaScript 在浏览器里最核心的一个特点,就是它的主执行环境通常是单线程的。 这意味着如果你在主线程里做了一个很重的任务,比如大文本解析、图片处理、复杂计算,页面的点击、滚动、输入框响应都有可能一起卡住。

所以浏览器提供了 Web Worker 这个机制。它的作用很直接:

  • 把一部分 JavaScript 任务放到后台线程去做
  • 主线程继续负责页面渲染和用户交互
  • 两边通过消息通信

TIPS:Web Worker 不是为了“让异步代码更异步”,而是为了把会占用主线程时间的工作挪走。像普通的 fetch 本身就已经是异步的,但它后续的数据处理、解析、状态管理,仍然可能对主线程产生影响。


1. 为什么需要 Web Worker

先看一个最简单的例子。下面这段代码会阻塞主线程:

button.addEventListener("click", () => {
console.log("开始计算");
let total = 0;
for (let i = 0; i < 1e9; i++) {
total += i;
}
console.log("计算完成", total);
});

这段代码执行时:

  • 页面会卡住
  • 按钮点不动
  • 输入框没反应
  • 动画会掉帧

原因很简单:JavaScript 主线程正在忙着算这件事,没空处理别的任务。

而 Web Worker 的思路就是把这种任务搬到后台去:

  • 主线程负责 UI
  • Worker 负责重活

2. 什么是 Web Worker

你可以把 Web Worker 理解成:

浏览器提供的一段“后台 JavaScript 运行环境”。

它和主线程是隔离的,有几个重要特点:

  • 有自己独立的执行上下文
  • 不能直接操作 DOM
  • 不能直接访问 window、document
  • 可以使用 fetch
  • 可以通过 postMessage 和主线程通信

这一点非常关键。因为它不能碰 DOM,所以它不适合写页面逻辑;但正因为隔离,它特别适合做纯计算和纯数据处理。


3. Web Worker 的基本使用方式

主线程中创建 Worker

const worker = new Worker("./worker.js");

如果你在现代构建工具里使用它,常见写法会是:

const worker = new Worker(new URL("./worker.js", import.meta.url));

这种写法在 Vite、Webpack、Next.js 的某些构建场景里更稳定,因为它能让打包器正确识别这个 Worker 文件。

主线程向 Worker 发消息

worker.postMessage({
type: "START_CALC",
payload: { count: 1000000 },
});

Worker 接收消息

self.addEventListener("message", (event) => {
const { type, payload } = event.data;
if (type === "START_CALC") {
let total = 0;
for (let i = 0; i < payload.count; i++) {
total += i;
}
self.postMessage({
type: "CALC_DONE",
payload: { total },
});
}
});

主线程接收 Worker 返回结果

worker.addEventListener("message", (event) => {
const { type, payload } = event.data;
if (type === "CALC_DONE") {
console.log("结果:", payload.total);
}
});

这个模式其实非常固定:

  1. 主线程创建 Worker
  2. 主线程发消息
  3. Worker 处理任务
  4. Worker 把结果发回来
  5. 主线程更新 UI

4. 一个简单的流程图

Web Worker 消息流程:主线程创建 Worker,发送任务,Worker 在后台处理,再把结果返回给主线程更新 UI。

5. Web Worker 适合做什么

一般来说,下面这些场景比较适合:

  • 大数据量计算
  • 文本解析和格式转换
  • 图片处理、音视频处理
  • 文件分块、压缩、解压
  • 大量 JSON 处理
  • 复杂算法执行

如果只是一个很轻的小函数,通常没必要上 Worker。因为创建 Worker、消息通信、数据拷贝本身也是有成本的。


6. Web Worker 不适合做什么

a. 直接操作页面

Worker 里不能这样写:

document.querySelector("#app").textContent = "Hello";

因为 Worker 根本拿不到 document。

b. 频繁处理很小的任务

如果任务本身很轻,主线程直接做反而更简单。因为:

  • Worker 要创建实例
  • 主线程和 Worker 之间要通信
  • 数据传递有序列化成本

c. 依赖浏览器页面上下文的逻辑

比如直接操作 DOM、读取元素尺寸、绑定点击事件,这些都应该留在主线程。


7. Worker 和异步的关系

这里有一个很容易混淆的点。

很多初学者会觉得:

  • async/await 是异步
  • fetch 是异步
  • 所以有异步就不需要 Worker 了

这个说法不完全对。

async/await 解决的是等待结果时不要把代码写成回调地狱。 但 Web Worker 解决的是不要让主线程去承担重任务。

换句话说:

  • Promise / async 主要解决代码组织问题
  • Web Worker 主要解决线程占用问题

这两个概念可以一起用,而且经常一起用。


8. 实际例子

我的AHF-QDS-EOE项目里就用了 Web Worker 来处理 AI 转换请求,相关代码在:

  • src/services/conversionService.ts
  • src/workers/conversion.worker.ts

整体思路是这样的:

  • 页面点击“开始 AI 转换”
  • 主线程不直接处理转换逻辑,而是调用 conversionService
  • conversionService 把请求发给 conversion.worker.ts
  • Worker 在后台发起 fetch
  • Worker 拿到结果后再把内容发回主线程
  • 主线程收到结果后更新状态

对应的主线程代码大致是:

this.worker!.postMessage({
type: "CONVERT_AI",
payload: {
...request,
requestId,
},
});

Worker 里则监听消息并执行请求:

self.addEventListener("message", async (event) => {
const { type, payload } = event.data;
if (type === "CONVERT_AI") {
const { baseUrl, apiKey, model, messages, requestId } = payload;
const response = await fetch(`${baseUrl}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages,
temperature: 0.7,
}),
});
const data = await response.json();
self.postMessage({
type: "CONVERT_SUCCESS",
payload: {
content: data.choices?.[0]?.message?.content || "",
requestId,
},
});
}
});

这个设计有两个直接好处:

  • 转换逻辑不会和页面组件强耦合
  • 主线程只负责发任务和收结果,结构更清晰

9. 使用 Web Worker 时常见的几个注意点

a. 传输的数据不要太随意

主线程和 Worker 之间传的是消息,不是共享普通变量。 如果你传超大的对象,通信成本会明显增加。

b. 给消息加 type

像这样:

worker.postMessage({
type: "PARSE_TEXT",
payload: text,
});

这样做的好处是后面任务一多,通信协议不会乱。

c. 给请求加 requestId

我的项目里就用了 requestId。 这样当多个任务并发执行时,主线程才能知道哪个结果对应哪个请求。

d. 记得处理错误

Worker 内部也可能失败,比如:

  • 网络失败
  • JSON 解析失败
  • 运行时异常

所以应该把错误也通过消息返回,而不是只处理成功路径。


10. 什么时候该考虑用 Web Worker

如果你遇到下面这些情况,就可以认真考虑:

  • 页面在某个操作后明显卡顿
  • 某个任务执行时间比较长
  • 任务本质上是纯计算或纯数据处理
  • 这个任务不需要直接操作 DOM

如果只是普通表单提交、轻量数据处理、简单状态更新,一般不用急着上 Worker。

前端开发里一个常见误区就是:看到“异步”就想上 Worker,或者看到“性能”就想上 Worker。其实应该先判断瓶颈到底在哪里。


11. 总结

Web Worker 的本质并不复杂。

你可以把它理解成:

  • 主线程负责界面
  • Worker 负责重活
  • 两边通过消息通信

它最适合的场景,是那些不需要操作页面、但又可能拖慢页面响应的任务。

而在实际项目里,Web Worker 往往不会单独出现。它经常会和下面这些技术搭配:

  • Promise / async-await
  • 状态管理
  • 持久化存储
  • 请求队列
  • 任务 ID 映射

这也是为什么在我的AHF-QDS-EOE项目里,最终能形成一种比较稳的体验:不是因为只用了 Web Worker,而是因为 Worker 和持久化状态一起工作了。

如果你刚开始学前端,我比较建议先把 postMessage 这一套通信模型吃透,再去看项目里的封装代码。这样你会更容易理解为什么很多工程代码表面上看起来复杂,但底层其实就是“发消息,处理消息,再把结果发回来”。