功能实现的差不多以后,我们做下代码的优化。
大家觉得我们的 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 没有了。
这样就达到了性能优化的目的。
当需要编译的文件多了之后,这种性能优化就更有意义。
