跳到主要内容

ReactPlayground项目实战:WebWorker性能优化

功能实现的差不多以后,我们做下代码的优化。

大家觉得我们的 playground 有啥性能瓶颈没有?

用 Performace 跑下就知道了:

用无痕模式打开这个页面,无痕模式下不会跑浏览器插件,比较准确。

打开 devtools,点击 reload 重新加载页面:

等页面渲染完点击停止,就可以看到录制的性能数据。

按住可以上下左右拖动,按住然后上下滑动可以放大缩小:

这里的 main 就是主线程:

主线程会通过 event loop 的方式跑一个个宏任务,也就是这里的 task。

超过 50ms 的被称为长任务 long task:

long task 会导致主线程一直被占据,阻塞渲染,表现出来的就是页面卡顿。

性能优化的目标就是消除这种 long task。

图中的宽度代表耗时,可以看到是 babelTransform 这个方法耗费了 24 ms

点击火焰图中的 babelTransform,下面会展示它的代码位置,点击可以跳到 Sources 面板的代码:

这就是我们要优化性能的代码。

这是 babel 内部代码,怎么优化呢?

其实这段代码就是计算量比较大,我们把它放到单独的 worker 线程来跑就好了,这样就不会占用主线程的时间。

vite 项目用 web worker 可以这样用:

我们用一下:

把 compiler.ts 改名为 compiler.worker.ts

然后在 worker 线程向主线程 postMessage

self.postMessage({
type: "COMPILED_CODE",
data: "xx",
});

主线程里创建这个 worker 线程,监听 message 消息:

import CompilerWorker from "./compiler.worker?worker";
const compilerWorkerRef = useRef<Worker>();

useEffect(() => {
if (!compilerWorkerRef.current) {
compilerWorkerRef.current = new CompilerWorker();
compilerWorkerRef.current.addEventListener("message", (data) => {
console.log("worker", data);
});
}
}, []);

跑一下:

可以看到,主线程接收到了 worker 线程传过来的消息。

反过来通信也是一样的 postMessage 和监听 message 事件。

主线程这边给 worker 线程传递 files,然后拿到 woker 线程传回来的编译后的代码:

import { useContext, useEffect, useRef, useState } from "react";
import { PlaygroundContext } from "../../PlaygroundContext";
import Editor from "../CodeEditor/Editor";
import iframeRaw from "./iframe.html?raw";
import { IMPORT_MAP_FILE_NAME } from "../../files";
import { Message } from "../Message";
import CompilerWorker from "./compiler.worker?worker";

interface MessageData {
data: {
type: string;
message: string;
};
}

export default function Preview() {
const { files } = useContext(PlaygroundContext);
const [compiledCode, setCompiledCode] = useState("");
const [error, setError] = useState("");

const compilerWorkerRef = useRef<Worker>();

useEffect(() => {
if (!compilerWorkerRef.current) {
compilerWorkerRef.current = new CompilerWorker();
compilerWorkerRef.current.addEventListener(
"message",
({ data }) => {
console.log("worker", data);
if (data.type === "COMPILED_CODE") {
setCompiledCode(data.data);
} else {
//console.log('error', data);
}
}
);
}
}, []);

useEffect(() => {
compilerWorkerRef.current?.postMessage(files);
}, [files]);

const getIframeUrl = () => {
const res = iframeRaw
.replace(
'<script type="importmap"></script>',
`<script type="importmap">${
files[IMPORT_MAP_FILE_NAME].value
}</script>`
)
.replace(
'<script type="module" id="appSrc"></script>',
`<script type="module" id="appSrc">${compiledCode}</script>`
);
return URL.createObjectURL(new Blob([res], { type: "text/html" }));
};

useEffect(() => {
setIframeUrl(getIframeUrl());
}, [files[IMPORT_MAP_FILE_NAME].value, compiledCode]);

const [iframeUrl, setIframeUrl] = useState(getIframeUrl());

const handleMessage = (msg: MessageData) => {
const { type, message } = msg.data;
if (type === "ERROR") {
setError(message);
}
};

useEffect(() => {
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, []);

return (
<div style={{ height: "100%" }}>
<iframe
src={iframeUrl}
style={{
width: "100%",
height: "100%",
padding: 0,
border: "none",
}}
/>
<Message type="error" content={error} />

{/* <Editor file={{
name: 'dist.js',
value: compiledCode,
language: 'javascript'
}}/> */}
</div>
);
}

而 worker 线程这边则是监听主线程的 message,传递 files 编译后的代码给主线程:

self.addEventListener("message", async ({ data }) => {
try {
self.postMessage({
type: "COMPILED_CODE",
data: compile(data),
});
} catch (e) {
self.postMessage({ type: "ERROR", error: e });
}
});

可以看到,拿到了 worker 线程传过来的编译后的代码:

预览也正常。

其实 files 变化没必要那么频繁触发编译,我们加个防抖:

useEffect(
debounce(() => {
compilerWorkerRef.current?.postMessage(files);
}, 500),
[files]
);

我们再用 performance 看下优化后的效果:

之前的编译代码的耗时没有了,现在被转移到了 worker 线程:

还是 24ms,但是不占据主线程了。

当然,因为我们文件内容很少,所以编译耗时少,如果文件多了,那编译耗时自然也就增加了,拆分就很有必要。

这样,性能优化就完成了。

然后再优化两处代码:

main.tsx 有个编辑器错误说 StrictMode 不是一个 jsx,这种不用解决,也不影响运行,改下模版把它去掉就行了:

上面那个只要编辑下文件就会触发类型下载,也不用解决:

再就是我们生成的文件名没必要 6 位随机数:

改为 4 位正好:

案例代码上传了小册仓库

总结

这节我们做了下性能优化。

我们用 Performance 分析了页面的 Event Loop,发现有 long task,性能优化的目标就是消除 long task

分析发现是 babel 编译的逻辑导致的。

我们通过 Web Worker 把 babel 编译的逻辑放到了 worker 线程跑,通过 message 事件和 postMessage 和主线程通信。

拆分后功能正常,再用 Performance 分析,发现耗时逻辑被转移到了 worker 线程,主线程这个 long task 没有了。

这样就达到了性能优化的目的。

当需要编译的文件多了之后,这种性能优化就更有意义。