场景
由于需要做一些 CPU 密集型的计算,为了优化性能,吾辈开始尝试使用 worker 将计算任务放到其它线程(主要还是为了避免主线程卡死)。
主要场景包括
- 浏览器上的
WebWorker/SharedWorker
:处理音频数据 - nodejs 中的
worker_threads
:解析 md/ts ast 然后处理
为什么不用 wasm?-- 主要是由于它需要从零开始编写相关的代码,而非可以直接对现有的 js 代码稍微修改便能提高性能。
浏览器
目前 vite 对此默认支持浏览器 worker,不再需要任何配置,仅使用 comlink 简化使用体验即可。
// hello.worker.ts
import { expose } from 'comlink'
export function hello(name: string) {
return `hello ${name}`
}
expose(hello)
// main.ts
import type { hello } from './hello.worker'
import HelloWorker from './hello.worker?worker'
import { wrap } from 'comlink'
;(async () => {
const asyncHello = wrap<typeof hello>(new HelloWorker())
const res = await asyncHello('liuli')
console.log(res)
})()
nodejs
nodejs 本身提供的解决方案
nodejs 仍然像浏览器一样基于文件系统使用 worker_threads,但官方确实提供了一种有趣的解决方案 -- 在同一个文件中包含主线程与 worker 线程的代码,然后在其中使用逻辑判断。
// worker.ts
import { isMainThread, parentPort, Worker } from 'worker_threads'
import { expose, wrap } from 'comlink'
import nodeEndpoint from 'comlink/dist/umd/node-adapter'
import { wait } from '@liuli-util/async'
import { worker as helloType } from './worker'
async function _hello(name: string) {
await wait(100)
return `hello ${name}`
}
let hello: (name: string) => Promise<string>
if (isMainThread) {
hello = async (name: string) => {
const worker = new Worker(__filename)
try {
const helloWorker = wrap<typeof helloType>(nodeEndpoint(worker))
return await helloWorker(name)
} finally {
worker.unref()
}
}
} else {
hello = _hello
expose(_hello, nodeEndpoint(parentPort!))
}
export { hello }
// main.ts
import { hello } from './worker.js'
;(async () => {
console.log(await mixingThreadHello('liuli'))
})()
看起来很简洁,但这种方案也并非尽善尽美,吾辈实际使用时发现以下问题
- 不支持开发环境测试 ts 代码,必须编译为 js 才能运行
- 由于基于文件系统,所以打包需要手动分块,或者不进行打包 rollup 参考 output.manualChunks
使用打包工具处理的调研方案
- rollup-plugin-web-worker-loader
- 默认不会处理 ts 文件
- worker 中包含依赖时不会打包
- rollup-plugin-worker-factory
- 没有看到 ts 的示例
- 默认会修改 Worker
- @surma/rollup-plugin-off-main-thread
- 没有看到 ts 的示例
目前没有找到满意的插件,后续可能不得不写一个 rollup 插件。还是那句话,没有对比就没有伤害,如果没有 vite 对 ts+worker 的良好支持,或许吾辈还能忍受这种糟糕的开发体验。
尝试编写 rollup 插件 rollup-plugin-worker-threads,以在 nodejs 中对 worker_threads
提供类似 vite 的开发体验。
大致使用方式如下
// src/util/wrapWorkerFunc.ts
import { expose, Remote, wrap } from 'comlink'
import path from 'path'
import { isMainThread, parentPort, Worker } from 'worker_threads'
import nodeEndpoint from 'comlink/dist/umd/node-adapter'
/**
* 包装需要放到 worker 中执行的函数
* 1. 当检查到当前文件不是 js 文件时会直接返回函数
* 2. 当检查到在主线程时执行时,使用 Worker 包装并执行它
* 3. 当检查到在 Worker 线程时,使用 expose 包装它然后执行
* 注:目前是每次都创建新的 Worker,也许可以考虑支持复用 Worker
* @param ep
*/
export function wrapWorkerFunc<T extends (...args: any[]) => any>(
ep: T,
): Remote<T> {
if (path.extname(__filename) !== '.js') {
return ep as Remote<T>
}
if (isMainThread) {
return ((...args: any[]) => {
const worker = new Worker(__filename)
const fn = wrap<T>(nodeEndpoint(worker))
return (fn(...args) as Promise<any>).finally(() => worker.unref())
}) as Remote<T>
}
expose(ep, nodeEndpoint(parentPort!))
return ep as Remote<T>
}
// src/hello.worker.ts
import { wait } from '@liuli-util/async'
import { wrapWorkerFunc } from './util/wrapWorkerFunc'
async function _hello(name: string) {
await wait(100)
return `hello ${name}`
}
export const hello = wrapWorkerFunc(_hello)
// index.ts
export * from './hello.worker'