• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

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

大家觉得我们的 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 没有了。

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

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

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
59.ReactPlayground项目实战:链接分享、代码下载
Next
61.ReactPlayground项目实战:总结