跳到主要内容

[Lv3] Web Worker 应用:后台运算不阻塞 UI

Web Worker 是一个在浏览器后台线程中运行 JavaScript 的 API,让你能够执行耗时的运算而不会阻塞主线程(UI 线程)。

核心概念

问题背景

JavaScript 原本是单线程的,所有代码都在主线程执行:

// ❌ 耗时运算阻塞主线程
function heavyComputation() {
for (let i = 0; i < 10000000000; i++) {
// 复杂计算
}
return result;
}

// 执行时整个页面冻结
const result = heavyComputation(); // UI 无法交互 😢

问题:

  • 页面卡住,用户无法点击、滚动
  • 动画停止
  • 用户体验极差

Web Worker 解决方案

Web Worker 提供多线程能力,让耗时任务在后台执行:

// ✅ 使用 Worker 在后台执行
const worker = new Worker('worker.js');

// 主线程不阻塞,页面依然可以交互
worker.postMessage({ data: largeData });

worker.onmessage = (e) => {
console.log('后台运算完成:', e.data);
};

情境 1:大数据处理

// main.js
const worker = new Worker('worker.js');

// 处理大型 JSON 数据
worker.postMessage({ data: largeDataArray, action: 'process' });

worker.onmessage = function (e) {
console.log('处理结果:', e.data);
};

// worker.js
self.onmessage = function (e) {
const { data, action } = e.data;

if (action === 'process') {
// 执行耗时的数据处理
const result = data.map((item) => {
// 复杂运算
return heavyComputation(item);
});

self.postMessage(result);
}
};

情境 2:图片处理

处理图片滤镜、压缩、像素操作等,避免冻结 UI。

情境 3:复杂计算

数学运算(如质数计算、加密解密) 大型文件的哈希值计算 数据分析和统计

使用限制与注意事项

不能在 Worker 中做的事

  • 直接操作 DOM
  • 访问 window、document、parent 对象
  • 使用某些 Web API(如 alert)

可以在 Worker 中使用

  • XMLHttpRequest / Fetch API
  • WebSocket
  • IndexedDB
  • 定时器(setTimeout、setInterval)
  • 部分浏览器 API
// 不适合用 Worker 的情况
// 1. 简单快速的运算(创建 Worker 本身有开销)
const result = 1 + 1; // 不需要 Worker

// 2. 需要频繁与主线程通信
// 通信本身有成本,可能抵消多线程优势

// 适合用 Worker 的情况
// 1. 单次长时间运算
const result = calculatePrimes(1000000);

// 2. 批次处理大量数据
const processed = largeArray.map(complexOperation);

实际项目应用案例

案例:游戏数据加密处理

在游戏平台中,我们需要对敏感数据进行加密/解密:

// main.js - 主线程
const cryptoWorker = new Worker('/workers/crypto-worker.js');

// 加密玩家数据
function encryptPlayerData(data) {
return new Promise((resolve, reject) => {
cryptoWorker.postMessage({
action: 'encrypt',
data: data,
key: SECRET_KEY,
});

cryptoWorker.onmessage = (e) => {
if (e.data.success) {
resolve(e.data.encrypted);
} else {
reject(e.data.error);
}
};
});
}

// 使用
const encrypted = await encryptPlayerData(sensitiveData);
// 页面不会卡顿,用户可以继续操作

// crypto-worker.js - Worker 线程
self.onmessage = function (e) {
const { action, data, key } = e.data;

try {
if (action === 'encrypt') {
// 耗时的加密运算
const encrypted = performHeavyEncryption(data, key);
self.postMessage({ success: true, encrypted });
}
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};

案例:大量游戏数据筛选

// 在 3000+ 款游戏中进行复杂筛选
const filterWorker = new Worker('/workers/game-filter.js');

// 筛选条件
const filters = {
provider: ['PG', 'PP', 'EVO'],
type: ['slot', 'live'],
minRTP: 96.5,
tags: ['popular', 'new'],
};

filterWorker.postMessage({
games: allGames, // 3000+ 款
filters: filters,
});

filterWorker.onmessage = (e) => {
displayGames(e.data.filtered); // 显示筛选结果
};

// 主线程不卡顿,用户可以继续滚动、点击

面试重点

常见面试问题

Q1: Web Worker 和主线程如何通信?

A: 通过 postMessageonmessage

// 主线程 → Worker
worker.postMessage({ type: 'START', data: [1, 2, 3] });

// Worker → 主线程
self.postMessage({ type: 'RESULT', result: processedData });

// 注意:数据会被「结构化克隆」(Structured Clone)
// 这意味着:
// ✅ 可以传递:Number, String, Object, Array, Date, RegExp
// ❌ 不能传递:Function, DOM 元素, Symbol

Q2: Web Worker 的性能开销是什么?

A: 主要有两个开销:

// 1. 创建 Worker 的开销(约 30-50ms)
const worker = new Worker('worker.js'); // 需要加载文件

// 2. 通信的开销(数据复制)
worker.postMessage(largeData); // 大数据复制耗时

// 解决方案:
// 1. 重用 Worker(不要每次都创建)
// 2. 使用 Transferable Objects(转移所有权,不复制)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]); // 转移所有权

Q3: Transferable Objects 是什么?

A: 转移数据所有权,而不是复制:

// ❌ 一般做法:复制数据(慢)
const largeArray = new Uint8Array(10000000); // 10MB
worker.postMessage(largeArray); // 复制 10MB(耗时)

// ✅ Transferable:转移所有权(快)
const buffer = largeArray.buffer;
worker.postMessage(buffer, [buffer]); // 转移所有权(毫秒级)

// 注意:转移后,主线程无法再使用该数据
console.log(largeArray.length); // 0(已转移)

支持的 Transferable 类型:

  • ArrayBuffer
  • MessagePort
  • ImageBitmap
  • OffscreenCanvas

Q4: 什么时候应该使用 Web Worker?

A: 使用决策树:

是否为耗时运算(> 50ms)?
├─ 否 → 不需要 Worker
└─ 是 → 继续判断

├─ 是否需要操作 DOM?
│ ├─ 是 → 不能用 Worker(考虑 requestIdleCallback)
│ └─ 否 → 继续判断

└─ 通信频率是否很高(> 60次/秒)?
├─ 是 → 可能不适合(通信开销大)
└─ 否 → ✅ 适合使用 Worker

适合的场景:

  • 加密/解密
  • 图片处理(滤镜、压缩)
  • 大数据排序/筛选
  • 复杂数学运算
  • 文件解析(JSON、CSV)

不适合的场景:

  • 简单计算(开销大于收益)
  • 需要频繁通信
  • 需要操作 DOM
  • 需要使用不支持的 API

Q5: Web Worker 有哪些类型?

A: 三种类型:

// 1. Dedicated Worker(专用)
const worker = new Worker('worker.js');
// 只能与创建它的页面通信

// 2. Shared Worker(共享)
const sharedWorker = new SharedWorker('shared-worker.js');
// 可以被多个页面/标签共享

// 3. Service Worker(服务)
navigator.serviceWorker.register('sw.js');
// 用于缓存、离线支持、推送通知

比较:

特性DedicatedSharedService
共享性单一页面多页面共享全站共享
生命周期随页面关闭最后一个页面关闭独立于页面
主要用途后台运算跨页面通信缓存、离线

Q6: 如何调试 Web Worker?

A: Chrome DevTools 支持:

// 1. 在 Sources 面板可以看到 Worker 文件
// 2. 可以设置断点
// 3. 可以在 Console 执行代码

// 实用技巧:在 Worker 中使用 console
self.addEventListener('message', (e) => {
console.log('Worker received:', e.data);
// 可以在 DevTools Console 看到
});

// 错误处理
worker.onerror = (error) => {
console.error('Worker error:', error.message);
console.error('File:', error.filename);
console.error('Line:', error.lineno);
};

性能对比

实测数据(处理 100 万条数据)

方法执行时间UI 是否卡顿内存峰值
主线程(同步)2.5 秒完全卡死250 MB
主线程(时间切片)3.2 秒偶尔卡顿280 MB
Web Worker2.3 秒完全流畅180 MB

结论:

  • Web Worker 不仅不卡 UI,还因为多核心并行而更快
  • 内存使用更少(主线程不需要保留大量数据)

相关技术

Web Worker vs 其他方案

// 1. setTimeout(伪异步)
setTimeout(() => heavyTask(), 0);
// ❌ 还是在主线程,会卡

// 2. requestIdleCallback(空闲时执行)
requestIdleCallback(() => heavyTask());
// ⚠️ 只在闲置时执行,不保证完成时间

// 3. Web Worker(真正多线程)
worker.postMessage(task);
// ✅ 真正并行,不阻塞 UI

Comlink 让 Worker 使用起来像普通函数:

// 传统方式(繁琐)
worker.postMessage({ action: 'add', a: 1, b: 2 });
worker.onmessage = (e) => console.log(e.data);

// 使用 Comlink(简洁)
import * as Comlink from 'comlink';

const worker = new Worker('worker.js');
const api = Comlink.wrap(worker);

// 像调用普通函数一样
const result = await api.add(1, 2);
console.log(result); // 3

学习建议

面试准备:

  1. 理解「为什么需要 Worker」(单线程问题)
  2. 知道「什么时候用」(耗时运算)
  3. 了解「通信机制」(postMessage)
  4. 认识「限制」(无法操作 DOM)
  5. 实现过至少一个 Worker 案例

实战建议:

  • 从简单案例开始(如质数计算)
  • 使用 Chrome DevTools 调试
  • 测量性能差异
  • 考虑 Comlink 等工具

相关主题