本周精读的文章是 speedy-introduction-to-web-workers,是一篇 Web Workers 快速入门的文章,借精读这篇文章的机会,谈谈对 Web Workers 的理解与运用。
就像分工,你只负责编码,而你的朋友负责设计,那你就可以专心把自己的事情做好,而且更快速的完成任务。
本文通过一个比方,描述了 Web Workers 的两大特征:
因为浏览器是单线程的,任何大量耗时的 JS 任务都会卡住界面,使浏览器无法响应任何操作,这样的用户体验非常糟糕。Web Workers 可以将耗时任务拆解出去,降低主线程的压力,避免主线程无响应。
但 CPU 资源是有限的,Web Workers 并不能增加总体运行效率,算上通信的损耗,整体计算效率会有一定的下降。
const worker = new Worker("../src/worker.js");
上述代码中,worker
就是一个 Web Workers 实例,执行的代码是 ../src/worker.js
路径下的文件。
Web Workers 用来执行异步脚本,只要掌握了它与主线程通信的方式,就可以在指定时机运行异步脚本,并在运行完时将结果传递给主线程。
const worker = new Worker("../src/worker.js");
worker.onmessage = e => {};
worker.postMessage("Marco!");
每个 worker
实例通过 onmessage
接收消息,通过 postMessage
发送消息。
self.onmessage = e => {};
self.postMessage("Marco!");
和主线程代码类似,在 Web Workers 代码中,也是 onmessage
接收消息,这个消息来自主线程或者其它 Workers。也可以通过 postMessage
发送消息。
worker.terminate();
文章内容就这么多,是不是有写太简单了呢!笔者结合自己的使用经验,再补充一些知识。
对象转移就是将对象引用零成本转交给 Web Workers 的上下文,而不需要进行结构拷贝。
这里要解释的是,主线程与 Web Workers 之间的通信,并不是对象引用的传递,而是序列化/反序列化的过程,当对象非常庞大时,序列化和反序列化都会消耗大量计算资源,降低运行速度。
上面的图充分证明了,大对象传递,使用对象转移各项指标都优于结构拷贝。
对象转移使用方式很简单,给 postMessage
增加一个参数,把对象引用传过去即可:
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
浏览器兼容性也不错:Currently Chrome 17+, Firefox, Opera, Safari, IE10+。更具体内容,可以看 Transferable Objects: Lightning Fast!。
需要注意的是,对象引用转移后,原先上下文就无法访问此对象了,需要在 Web Workers 再次将对象还原到主线程上下文后,主线程才能正常访问被转交的对象。
Web Workers 优势这么大,但用起来需要在同域下创建一个 JS 文件实在不方便,尤其在前后端分离做的比较彻底的团队,前端团队能控制的仅仅是一个 JS 文件。那么下面给出几个不用 JS 文件,就创建 Web Workers 的方法:
worker-loader 是一个 webpack 插件,可以将一个普通 JS 文件的全部依赖提取后打包并替换调用处,以 Blob 形式内联在源码中。
import Worker from "worker-loader!./file.worker.js";
const worker = new Worker();
上述代码的魔术在于,转化成下面的方式执行:
const blob = new Blob([codeFromFileWorker], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
第二种方式由第一种方式自然带出:如果不想用 webpack 插件,那自己通过 Blob 的方式创建也可以:
const code = `
importScripts('https://xxx.com/xxx.js');
self.onmessage = e => {};
`;
const blob = new Blob([code], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
看上去代码更轻量一些,不过问题是当遇到复杂依赖时,如果不能把依赖都转化为脚本通过 importScripts
方式引用,就无法访问到主线程环境中的包。如果真的遇到了这个问题,可以用第一种 webpack 插件的方式解决,这个插件会自动把文件所有依赖都打包进源码。
为什么 postMessage 会形成队列,为什么要管理它?
首先在 Web Workers 架构设计上就必须做成队列,因为调用 postMessage
时,对应的 Web Workers 不一定完成了初始化,所以浏览器底层必须管理一个队列,在 Web Workers 初始化完毕时,依次消费,这样才能确保任何时候发出的 postMessage
都能被 Web Workers 接收到。
其次,为什么要手动维护这个队列,原因可能取决于如下几点:
postMessage
还没来得及消费,就不要发送新的消息,或者丢弃新的消息,这时候需要通过双向通信拿到 Web Workers 的执行结果回执,手动控制队列。如上图所示,对于每次用户输入都要进行的 SQL Parser 很耗时,及时放在 Web Workers 也可能导致将 Workers 撑爆到无响应,这是不仅要使用多 Workers 缓冲池,还要对待执行队列进行过滤,因为用户永远只关心最后一次输入的 Parser 结果。
由于 Web Workers 运算被卡住时,除了销毁 Worker 没有别的办法,而销毁 Worker 的成本比较高,不能对每一个用户输入都销毁并新建 Web Workers,所以利用 Workers 缓冲池,当缓冲池满了,新的消费队列又进来的时候,可以销毁全部 Workers 缓冲池,换一批新缓冲池重新消费用户输入。
Web Workers 是拆解异步计算的好帮手,vscode 网页版也通过 Web Workers 异步完成代码提示和高亮,笔者有对比过,发现 Web Workers 性能提升非常明显。
管理好你的 Web Workers 消息队列,谨防同步计算让 Web Workers 失去响应,建立一个智能的消息队列,根据业务需求设计一个最好的队列消费模型吧!
讨论地址是:精读《谈谈 Web Workers》 · Issue #108 · dt-fe/weekly