我猜你需要Web Worker来解决你的性能问题

316 阅读4分钟

众所周知,JavaScript 是单线程的,但是随着 Web 技术的发展,我们越来越需要在 Web 应用中处理大量的计算任务,比如图像处理、数据分析等。为了解决这个问题,Web Workers 应运而生。Web Worker 是浏览器提供的一种 JavaScript 多线程机制,允许在主线程之外运行代码,从而避免阻塞 UI 渲染。

Web Worker 基础使用

我们可以使用 new Worker(url) 来创建一个 Web Worker,其中 url 是一个 JavaScript 文件的路径,该文件包含了需要在 Web Worker 中运行的代码。例如:

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

// 错误处理
worker.onerror = function (event) {
  console.error("Error in Worker: ", event.message);
};

// 终止 Worker
worker1.terminate();

注意: Worker 线程无法读取本地文件,所以不能打开本机的文件系统(file://)所加载的脚本,必须来自网络。

Worker 线程与主线程通信

Web Worker 与主线程之间可以通过 postMessageonmessage 事件进行通信。主线程可以通过 postMessage 方法向 Worker 线程发送消息,Worker 线程可以通过 onmessage 事件监听主线程发送的消息。例如:

mian.js

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

worker.onmessage = function (event) {
  console.log("Received message from worker:", event.data);
};

worker.postMessage("Hello, worker!");

worker.js

// Worker 线程
self.onmessage = function (event) {
  console.log("Received message from main thread:", event.data);
  self.postMessage("Hello, main thread!");
};

Worker 中的 API

self.close(); // 关闭 Worker
self.postMessage(message, [transfer]); // 向主线程发送消息
self.onmessage = function (event) {}; // 监听主线程发送的消息
self.onerror = function (event) {}; // 监听 Worker 线程中的错误
self.importScripts(url); // 导入其他脚本

Worker 线程与主线程共享数据

一般情况下,Web Worker 与主线程之间无法直接共享数据,但可以通过几种方式解决

默认数据传递(结构化克隆)

就是上面刚说到的通过 postMessage 传递数据,通过这种方式传递的数据会被深拷贝到目标线程,也就是说这种通信是拷贝关系,是传值而不是传址,线程对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。

注意 :postMessage 方法只能传递可序列化的数据,对于不可序列化的数据(如函数、DOM 元素等)无法传递。

Transferable objects(转移)

ArrayBufferFileBlob这些也是可以直接传递给线程的,但是这些二进制数据在拷贝的过程中,性能开销较大,所以浏览器提供了 Transferable objects 语义,允许主线程把数据所有权转交给 Worker 线程,两者之间不再发生数据拷贝,也就不会发生阻塞主线程的情况。

mian.js

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

const buffer = new ArrayBuffer(1024);
console.log("初始化buffer: ", buffer.byteLength); // 1024
worker.postMessage(buffer, [buffer]); // // 转移后,主线程无法再访问 buffer
console.log("传递后buffer: ", buffer.byteLength); // 0

worker.js

// Worker 线程
self.onmessage = function (event) {
  const buffer = event.data; // 可直接获取到原始的buffer
  console.log(buffer.byteLength); // 1024
};

SharedArrayBuffer

SharedArrayBuffer 对象

SharedArrayBuffer 是一种特殊的 ArrayBuffer,允许多个线程共享同一块内存。主线程和 Worker 线程都可以通过 SharedArrayBuffer 对象来访问和修改共享内存,从而实现数据的共享和同步。但是需要注意必须满足以下两点:

  1. 安全的上下文 必须在 HTTPS 或 localhost 下使用
  2. 垮源隔离 需要在响应头中设置:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

mian.js

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

const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Uint8Array(sharedBuffer);

for (let i = 0; i < sharedArray.length; i++) {
  sharedArray[i] = i;
}

worker.postMessage(sharedBuffer);

worker.js

// Worker 线程
self.onmessage = function (event) {
  const sharedBuffer = event.data;
  const sharedArray = new Uint8Array(sharedBuffer);
  console.log(sharedArray);
};

同页面 Web Worker

我们刚开始说过,new Worker(url) 需要一个 js 文件路径,但是我们也可以直接在当前页面创建一个 Worker,只需要将 new Worker() 改为 new Worker(URL.createObjectURL(new Blob([workerSource], { type: "text/javascript" })),其中 workerSource 是 Worker 的代码,例如:

const workerSource = `
  self.onmessage = function (event) {
    console.log("Received message from main thread:", event.data);
    self.postMessage("Hello, main thread!");
}`;

const worker = new Worker(
  URL.createObjectURL(new Blob([workerSource], { type: "text/javascript" }))
);
worker.onmessage = function (event) {
  console.log("Received message from worker:", event.data);
};

worker.postMessage("Hello, worker!");

当然我们在 html 中也可以使用 script 标签来创建一个 Web Worker,例如:

<script id="worker" type="app/worker">
  self.onmessage = function (event) {
    console.log("Received message from main thread:", event.data);
    self.postMessage("Hello, main thread!");
  };
</script>
<script>
  const objURL = URL.createObjectURL(
    new Blob([document.querySelector("#worker").textContent])
  );
  const worker = new Worker(objURL);
  worker.postMessage("Hello, worker!");
  worker.onmessage = function (event) {
    console.log("Received message from worker:", event.data);
  };
</script>

Web Worker 的限制

  • 无法访问 DOM

    Worker 无法直接操作 DOM(如 documentwindow 对象)。

  • 无法访问全局变量

    Worker 环境中的全局对象是 self,不是 window,并且部分浏览器 API 不可用,例如: localStorage(但支持 IndexedDB)。 alert()confirm() 等界面交互方法。

  • 参数传递限制

    postMessage 传递的数据必须是可序列化的,如果传递的参数是函数则直接报错,DOM 元素也是。

  • 同源策略限制

    Worker 脚本必须与主线程页面同源(协议、域名、端口完全一致),且禁止从本地文件加载 Worker,需通过服务器(如 localhost)运行。

  • 资源消耗与管理

    每个 Worker 占用独立线程和内存,创建过多 Worker 会导致系统资源耗尽,线程切换开销增加,反而降低性能。可以使用workerpool 线程池来管理线程。

  • 请求

    Web Worker 环境中没办法使用一些 axios 或者 jQueryajax,我们只能使用原生的 fetch,或者使用 XMLHttpRequest 来解决该问题。