使用模块 worker 进行 Web 线程处理

现在,借助 Web Worker 中的 JavaScript 模块,可以更轻松地将繁重的工作转移到后台线程中。

JavaScript 是单线程的,这意味着一次只能执行一项操作。这种方法非常直观,并且适用于 Web 上的许多情况,但如果我们需要执行繁重的任务(例如数据处理、解析、计算或分析),则可能会出现问题。随着 Web 应用越来越复杂,对多线程处理的需求也水涨船高。

在 Web 平台上,线程处理和并行处理的主要基元是 Web Workers API。工作器是基于操作系统线程的轻量级抽象,用于提供用于线程间通信的消息传递 API。在执行成本高昂的计算或对大型数据集执行操作时,此功能非常有用,可以让主线程顺利运行,同时在一个或多个后台线程上执行成本高昂的操作。

以下是一个典型的工作器使用示例,其中工作器脚本监听来自主线程的消息,并通过发回自己的消息进行响应:

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

Web Worker API 已在大多数浏览器上推出超过十年。虽然这意味着 worker 具有出色的浏览器支持并且经过充分优化,但这也意味着它们比 JavaScript 模块早。由于在设计 worker 时没有模块系统,因此用于将代码加载到 worker 并编写脚本的 API 仍然与 2009 年常见的同步脚本加载方法类似。

历史记录:传统版 worker

worker 构造函数采用传统脚本网址,该网址相对于文档网址。它会立即返回对新工作器实例的引用,这会公开消息传递接口以及立即停止和销毁工作器的 terminate() 方法。

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

Web 工作器内提供了 importScripts() 函数,用于加载其他代码,但它会暂停工作器的执行,以便获取和评估每个脚本。此外,它还会在全局范围内执行脚本(就像经典的 <script> 标记一样),这意味着一个脚本中的变量可能会被另一个脚本中的变量覆盖。

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

因此,Web 工作器历来对应用架构施加了巨大影响。开发者不得不开发出巧妙的工具和解决方法,以便在不放弃现代开发做法的情况下使用 Web Worker。例如,webpack 等捆绑器将小型模块加载器实现嵌入到生成的代码中,该实现使用 importScripts() 进行代码加载,但会将模块封装在函数中,以避免变量冲突并模拟依赖项导入和导出。

输入模块工作器

Chrome 80 中推出了一种适用于 Web 工作器的新模式,该模式称为模块工作器,该模式具有 JavaScript 模块的工效学设计和性能优势。Worker 构造函数现在接受新的 {type:"module"} 选项,该选项会更改脚本加载和执行以匹配 <script type="module">

const worker = new Worker('worker.js', {
  type: 'module'
});

由于模块 Worker 是标准的 JavaScript 模块,因此它们可以使用 import 和 Export 语句。与所有 JavaScript 模块一样,依赖项仅在给定上下文(主线程、工作器等)中执行一次,并且将来的所有导入都会引用已执行的模块实例。浏览器也优化了 JavaScript 模块的加载和执行。可以在执行模块之前加载模块的依赖项,这样便可以并行加载整个模块树。模块加载还会缓存已解析的代码,也就是说,在主线程和 worker 中使用的模块只需解析一次。

移至 JavaScript 模块还支持使用动态导入来延迟加载代码,而不会阻止 worker 的执行。动态导入比使用 importScripts() 加载依赖项更明确,因为系统会返回导入模块的导出内容而不是依赖于全局变量。

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

为了确保出色的性能,旧的 importScripts() 方法在模块工作器中不可用。将 worker 切换为使用 JavaScript 模块意味着所有代码都将在严格模式下加载。另一个值得注意的变化是,在 JavaScript 模块的顶级作用域中,this 的值为 undefined,而在传统 worker 中,该值则是 worker 的全局范围。幸运的是,始终有一个 self 全局变量能够提供对全局范围的引用。它适用于所有类型的 worker(包括 Service Worker)以及 DOM。

使用 modulepreload 预加载 worker

模块 worker 附带的一项重大性能改进是能够预加载 worker 及其依赖项。借助模块工作器,脚本将作为标准 JavaScript 模块加载和执行,这意味着可以使用 modulepreload 预加载甚至预解析这些模块:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

主线程和模块 worker 也可以使用预加载的模块。这对于在这两种上下文中导入的模块非常有用,或者在某些情况下无法预先知道某个模块将用于主线程上还是工作器中。

以前,可用于预加载 Web 工作器脚本的选项有限,不一定可靠。传统版 worker 具有自己的用于预加载的“worker”资源类型,但没有浏览器实现 <link rel="preload" as="worker">。因此,可用于预加载 Web 工作器的主要技术是使用 <link rel="prefetch">,它完全依赖于 HTTP 缓存。与正确的缓存标头结合使用时,这样可以避免工作器实例化必须等待下载工作器脚本。不过,与 modulepreload 不同的是,此方法不支持预加载依赖项或准备解析。

共享工作器怎么样?

从 Chrome 83 开始,共享工作器已更新,可支持 JavaScript 模块。与专用工作器一样,使用 {type:"module"} 选项构建共享工作器现在会将工作器脚本作为模块(而不是经典脚本)加载:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

在支持 JavaScript 模块之前,SharedWorker() 构造函数只需要一个网址和一个可选的 name 参数。这适用于传统共享工作器;但是,创建模块共享工作器需要使用新的 options 参数。可用选项与专用工作器的相同,包括取代之前的 name 参数的 name 选项。

那么 Service Worker 呢?

Service Worker 规范已更新,以支持使用与模块工作器相同的 {type:"module"} 选项接受 JavaScript 模块作为入口点,但这项更改尚未在浏览器中实现。一旦发生这种情况,可以使用以下代码通过 JavaScript 模块实例化 Service Worker:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

既然该规范已更新,那么浏览器将开始实现这种新行为。这需要一些时间,因为将 JavaScript 模块引入 Service Worker 会带来一些额外的复杂问题。在确定是否触发更新时,Service Worker 注册需要将导入的脚本与之前的缓存版本进行比较,当用于 Service Worker 时,需要为 JavaScript 模块实现这一点。此外,在某些情况下,在检查更新时,Service Worker 需要能够绕过脚本缓存

其他资源和补充阅读材料