外观
第60章—ReactPlayground项目实战:WebWorker性能优化
1822字约6分钟
2024-09-19
功能实现的差不多以后,我们做下代码的优化。
大家觉得我们的 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 没有了。
这样就达到了性能优化的目的。
当需要编译的文件多了之后,这种性能优化就更有意义。