好久不见
前言
相信 web worker 对大家来说也不是个新东西了,笔者今天要讲的 web worker 和 react component 之间的事情其实也不是什么新东西了。
但是相信很多人其实并没有很了解其中的概念,因为相关的文章太少了,在掘金社区中搜索目前也只有 @LucasHC 大佬讲过我所了解的那些东西。
很多人认为这个方案不可行因为很多测试结果表示简单的页面上 worker 和 主线程 之间的通信时间的花费还不如直接在主线程渲染 react 组件,但实际上很多的业务是足够复杂的:
各位打工人可以想想自家的 shit 山是不是有很难优化的运行时瓶颈),web worker 或许能起到你意想不到的优化效果(画🫓 ):)
本文只是一篇发散思考的文章,探索 react-worker-component 的原理和其背后的思想,希望也能为诸君带来一些启发。
免责声明:本文代码为了方便理解仅作示例用,不保证可运行 🤣
当 worker 经常被这样使用
示例
想一想,如果让你在 react 中使用 worker 优化性能,你是不是大概会这样做:
javascript
const worker = new Worker(workerJsUrl);
function NoteComonent({sourceData}) {
const [props, setProps] = useState(null)
useEffect(() => {
const updater = (e) => {
setProps(e.data);
};
worker.postMessage(sourceData)
worker.addEventListener('message', updater)
return () => { worker.removeEventListener('message', updater) }
}, [sourceData]);
return props ? <Detail {...props} /> : <p>loading...</p>
}
实际上,这个过程中我们并没有结合 react,只是在做"使用 worker 处理数据"这件事情

问题
这个写法有问题吗?--- 完全没有任何问题
但是实际的项目中并不总是给你一个书写全新的业务逻辑的任务,很多时候你需要优化那些可怕的老代码,这时候还使用上面的方式就要面临一个巨大的难题:
你需要将老代码的数据处理逻辑全部都抽离到 worker js 中,同时原有的渲染组件也可能要改造以适配对应的模式
如果上面的难题对你来说并不是件难事,那么恭喜你遇到了一个没那么复杂 / 维护很好 的项目,但改造总是有成本、风险的问题的,那么还有什么方式呢?请看下文
React worker component
react-worker-component 是开源牛人 dai-shi (zustand、jotai、waku 等著名库作者)受 RSC 启发所做的一次探索性尝试,github 地址:github.com/dai-shi/rea...
原理介绍,和上述所说的使用方式不同,他采用的方式如下:
-
React component 逻辑完全运行在 worker 里面并且最终也是生成了 JSX 数据
-
JSX 数据通过 序列化****和反序列化 传递到主线程
-
由于反序列化后的 JSX 和正常的 JSX 没什么两样,可以自然被 React Vdom 体系收纳运行

如何序列化 JSX
上图的其他部分大家都能理解,但是可能会对 JSX 的序列化和反序列化感到无比的迷惑,JSX 是 react 生成的特定对象,主线程和 worker 又不能变量共享,怎么可能做到呢?
恭喜你!和你想的一样,能序列化说明肯定是以纯数据的传输的,那么 JSX 到底是什么样的数据呢,不妨在控制台打印看看:

其实组成一个组件的关键因素就两个:type 和 props
scss
type(props) = JSX.Element
所以 dai-shi 使用的方法大概是这样的(笔者为了方便理解的极简版实现)
序列化:主要逻辑是处理了 jsx element 类型的情况下的 type 和 props
javascript
// serialize 序列化所有 jsx 中可能包含的类型,本代码仅有 obj 和 element,原库还实现了 array、null 和基本类型等等
function serialize(x) {
if(x.$$typeof === Symbol.for('react.element')) {
const e = {
// 序列化 props,采用递归因为 props 中可能有 children 而并只是纯数据
props: serialize(x.props),
// 这里假设所有的 type 都是 html 元素 (原库还实现了内嵌 react 组件)
type: x.type,
};
// 细节:使用 e 而不是 element 主要是减少序列化的 size
return { e };
}
if (Object.getPrototypeOf(x) === Object.prototype) {
const o = {};
Object.entries(x).forEach(([key, val]) => {
// val 可能是 element 也可能是别的什么
o[key] = serialize(val);
});
return { o };
}
}
反序列化:主要逻辑是将序列化出来的 e 重新运行 react 的 createElement 方法生成 JSX.Element
ini
export const deserialize = (x) => {
if ("e" in x) {
// 关键点:使用 createElement 重新生成 JSX.Element
const ele = createElement(
x.e.type,
deserialize(x.e.props),
);
return ele;
}
if ("o" in x) {
const obj = {};
Object.entries(x.o).forEach(([key, val]) => {
obj[key] = deserialize(val);
});
return obj;
}
};
看到这里相信你豁然开朗了,原来也不过如此。但是我们要意识到,这样做之后理想情况下原有的 react 组件无需做任何的变更便可以直接使用 worker 去加速渲染!
别急着评论,看看这段
当然你可能会吐槽上面的方式也有很多问题,如 只能渲染纯展示组件没有任何交互能力、主线程再运行一遍 createElement 纯属脱裤子放 P 等等
这里我想谈谈我的理解:
首先,这个库是 dai-shi 受到 react server component 启发所做,不了解 RSC 的建议好好看看 react 官方帖子中的视频,你肯定能豁然开朗:
RSC 在 2020 年的时候已经在 facebook 的真实环境灰度使用过,并且直到今天为止 react team 依然对于 RSC 抱着积极、活跃的态度,所以这个方向肯定是可行、有使用场景的。
-
RSC 也必然遇到序列化和反序列化带来的一系列问题,同时必然会提供框架层面的解决方案
-
可交互问题可能没有想象那么严重,RSC demo 中也示例了交互组件对应的构建和协同形式,也有望在框架层面磨平开发体验
-
createElement 主线程虽然又执行了一次,但是很多复杂的项目优化到这个级别已经有了巨大的提升了(况且这块损耗随着 react team 的优化也有望缩减到更少)
react-worker-component 确实不是一个已经 ready 的方案,但是其思路完全就是 RSC 的衍生,值得我们借鉴学习。
很久以后,或许 react 不仅是 react component 构建出来的,而是由 RSC、RWC、RNC...等共同协作构建而成
后记
笔者认为这将是一个应用的全新构建模式,或许会彻底改变前端应用的体验和应用开发的分工,更不必说可能带来很多衍生技术,笔者也会继续学习、尝试和分享相关的技术,欢迎同好交流、关注。