前言—
Web Workers
是 2009 年就已经提案的老技术,但是在很多项目中的应用相对较少,常见一些文章讨论如何写 demo ,但很少有工程化和项目级别的实践,本文会结合Web Workers
在京东羚珑的程序化设计项目中的实践,分享一下在当下的 2023 年,关于worker
融入项目的一些思考和具体的实现方式,涉及到的 demo 已经放在 github 上附在文末,可供参考。
先简单介绍下Web Workers
,它是一种可以运行在 Web 应用程序后台线程,独立于主线程之外的技术。众所周知,JavaScript 语言是单线程模型的,而通过使用Web Workers
,我们可以创造多线程环境,从而可以发挥现代计算机的多核 CPU 能力,在应对规模越来越大的 Web 程序时也有较多收益。
Web Workers 宏观语义上包含了三种不同的 Worker:DedicatedWorker(专有worker)
、SharedWorker(共享Worker)
、ServiceWorker
,本文讨论的是第一种,其他两种大家可以自行研究一下。
引入 Web Worker—
当引入新技术时,通常我们会考虑的问题有:1、兼容性如何?2、使用场景在哪?
问题 1,Web Workers 是 2009 年的提案,2012 年各大浏览器已经基本支持,11 年过去了,现在使用已经完全没有问题啦
问题 2,主要考虑了以下 3 点:
Worker API
的局限性:同源限制、无 DOM 对象、异步通信,因此适合不涉及 DOM 操作的任务Worker
的使用成本:创建时间 + 数据传输时间;考虑到可以预创建,可以忽略创建时间,只考虑数据传输成本,这里可参考 19 年的一个测试 Is postMessage slow [1] ,简要结论是比较乐观的,大部分设备和数据情况下速度不是瓶颈任务特点:需要是可并行的多任务,为了充分利用多核能力,可并行的任务数越接近 CPU 数量,收益会越高。多线程场景的收益计算,可以参考
Amdahl
公式,其中F
是初始化所需比例,N
是可并行数:
综上结论是,可并行的计算密集型任务适合用Worker
来做。
不过 github 上我搜罗了一圈,也发现有一些不局限于此,颇有创意的项目,供大家打开思路:
redux 挪到了 worker 内 [2]
dom 挪到了 worker 内 [3]
可使用多核能力的框架 [4]
介绍完worker
,一个问题出现了:为什么一个兼容性良好,能够发挥并发能力的技术(听起来很有诱惑力),到现在还没有大规模使用呢?
我理解有 2 个原因:一是暂无匹配度完美的使用场景,因此引入被搁置了;二是worker api
设计得太难用,参考很多 demo 看,限制多配置还麻烦,让人望而却步。本文会主要着力于第二点,希望给大家的worker
实践提供一些成熟的工程化思路。
至于第一点理由,在如此卷的前端领域,当你手中已经有了一把好用的锤子,还找不到那颗需要砸的钉子吗?
Worker 到底有多难用
下面是一个原始worker
的调用示例,上面是主线程文件,下面是worker
文件:
// index.js
const worker = new Worker('./worker.js')
worker.onmessage = function (messageEvent) {
console.log(messageEvent)// worker.js
importScripts('constant.js')
function a() {
console.log('test')
其中问题有:
postMessage
传递消息的方式不适合现代编程模式,当出现多个事件时就涉及分拆解析和解决耦合问题,因此需要改造新建
worker
需要单独文件,因此项目内需要处理打包拆分逻辑,独立出worker
文件worker
内可支持定义函数,可通过importScript
方式引入依赖文件,但是都独立于主线程文件,依赖和函数的复用都需要改造多线程环境必然涉及同步运行多个
worker
,多worker
的启动、复用和管理都需要自行处理
看完这么多问题,有没有感觉头很大,一个设计这样原始的 api,如何舒服的使用呢?
类库调研
首先可以想到的就是借助成熟类库的力量,下面表格是较为常见的几款worker
类库,其中我们可能会关注的关键能力有:
通信是否有包装成更好用的方式,比如
promise
化或者rpc
化是否可以动态创建函数——可以增加
worker
灵活性是否包含多
worker
的管理能力,也就是线程池考虑
node
的使用场景,是否可以跨端运行
![](https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2023%2F0227%2F8f73da17j00rqqrq5001fd200u000acg00wh00b6.jpg&thumbnail=660x2147483647&quality=80&type=jpg)
比较之下,workerpool[5] 胜出,它也是个年纪很大的库了,最早的代码提交在 6 年前,不过实践下来没有大问题,下文都会在使用它的基础上继续讨论。
有类库加持的 worker 现状
通过使用workerpool
,我们可以在主线程文件内新建worker
;它自动处理多worker
的管理;可以执行worker
内定义好的函数a
;可以动态创建一个函数并传入参数,让worker
来执行。
// index.js
import workerpool from 'workerpool'
const pool = workerpool.pool('./worker.js')
// 执行一个 worker 内定义好的函数
pool.exec('a', [1, 2]).then((res) => {
console.log(res)
// 执行一个自定义函数
pool
.exec(
(x, y) => {
return x + y
}, // 自定义函数体
[1, 2], // 自定义函数参数
.then((res) => {
console.log(res)// worker.js
importScripts('constant.js')
function a() {
console.log('test')
但是这样还不够,为了可以舒适的写代码,我们需要进一步改造。
向着舒适无感的 worker 编写前进
我们期望的目标是:
足够灵活:可以随意编写函数,今天我想计算
1+1
,明天我想计算1+2
,这些都可以动态编写,最好它可以直接写在主线程我自己的文件里,不需要我跑到worker
文件里去改写;足够强大:我可以使用公共依赖,比如
lodash
或者是项目里已经定义好的某些公共函数。
考虑到workerpool
具备了动态创建函数的能力,第一点已经可以实现;而第二点关于依赖的管理,则需要自行搭建,接下来介绍搭建步骤。
抽取依赖,管理编译和更新:
新增一个依赖管理文件worker-depts.js
,可按照路径作为 key 名构建一个聚合依赖对象,然后在worker
文件内引入这份依赖
// worker-depts.js
import * as _ from 'lodash-es'
import * as math from '../math'
const workerDepts = {
_,
'util/math': math,
}
export default workerDepts
// worker.js
import workerDepts from '../util/worker/worker-depts'
定义公共调用函数,引入所打包的依赖并串联流程:
worker
内定义一个公共调用函数,注入 worker-depts 依赖,并注册在workerpool
的方法内
// worker.js
import workerDepts from '../util/worker/worker-depts'
function runWithDepts(fn: any, ...args: any) {
var f = new Function('return (' + fn + ').apply(null, arguments);')
return f.apply(f, [workerDepts].concat(args))
}
workerpool.worker({
runWithDepts,
})
主线程文件内定义相应的调用方法,入参是自定义函数体和该函数的参数列表
// index.js
import workerpool from 'workerpool'
export async function workerDraw(fn, ...args) {
const pool = workerpool.pool('./worker.js')
return pool.exec('runWithDepts', [String(fn)].concat(args))
完成以上步骤,就可以在项目任意需要调用worker
的位置,像下面这样,自定义函数内容,引用所需依赖(已注入在函数第一个参数),进行使用了。
这里我们引用了一个项目内的公共函数fibonacci
,也引用了一个lodash
的map
方法,都可以在depts
对象上取到
// 项目内需使用worker时
const res = await workerDraw(
(depts, m, n) => {
const { map } = depts['_']
const { fibonacci } = depts['util/math']
return map([m, n], (num) => fibonacci(num))
},
input1,
input2,
优化语法支持
没有语法支持的依赖管理是很难用的,通过对workerDraw
进行ts
语法包装,可以实现在使用时的依赖提示:
import workerpool from 'workerpool'
import type TDepts from './worker-depts'
export async function workerDraw(fn: (depts: typeof TDepts, ...args: T) => Promise | R, ...args: T) {
const pool = workerpool.pool('./worker.js')
return pool.exec('runWithDepts', [String(fn)].concat(args))
}
然后就可以在使用时获取依赖提示:
其他问题
新增了worker
以后,出现了window
和worker
两种运行环境,如果你恰好和我一样需要兼容node
端运行,那么运行环境就是三种,原本我们通常判断 window 环境使用的也许是typeof window === 'object'
这样,现在不够用了,这里可以改为 globalThis 对象,它是三套环境内都存在的一个对象,通过判断globalThis.constructor.name
的值,值分别是'Window' / 'DedicatedWorker'/ 'Object'
,从而实现环境的区分
总结—
通过使用workerpool
,添加依赖管理和构建公共worker
调用函数,我们实现了一套按需调用,灵活强大的worker
使用方式。
在京东羚珑的程序化设计项目中,通过把 skia 图形绘制部分逐步改造为worker
内调用,我们实现了整体服务耗时降低 75% 的效果,收益还是非常不错的。
文中涉及的代码示例都已放在 github[6] 上,内有vite
和webpack
两个完整实现版本,感兴趣的小伙伴可以 clone 下来参照着看~
参考资料—
Is postMessage slow: https://dassur.ma/things/is-postmessage-slow/
[2]
redux 挪到了 worker 内: https://blog.axlight.com/posts/off-main-thread-react-redux-with-performance
[3]
dom 挪到了 worker 内: https://github.com/ampproject/worker-dom
可使用多核能力的框架: https://github.com/neomjs/neo
[5]
workerpool: https://github.com/josdejong/workerpool
[6]
github: https://github.com/Silencesnow/worker-demo-2022
[7]
MDN Web Workers API: https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API
[8]
workerpool: https://github.com/josdejong/workerpool
[9]
前端项目上 Web Worker 实践: https://www.youtube.com/watch?v=AEpG-3XXrjk
[10]
Web Worker 文献综述: https://juejin.cn/post/6854573213297410062
《2022 中国开源开发者报告》下载
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.