Vite + React 解决打包产物单个文件大小超出限制的问题

一、前言

最近在一个 Vite + React 项目打包部署的过程中遇到了如下图所示的警告,提示打包产物中单个文件大小超出了默认 500kb 的限制。可以看到打包产物中只有一个 html/css/js 文件,其中包含了整个应用程序的所有代码,体积庞大也是理所当然。

随着打包产物单个文件的体积越来越庞大,会出现一系列应用加载性能问题,如首页出现长时间白屏,严重影响用户体验。

针对这个问题 Vite 也给出了三种解决方案:

  1. 使用动态导入进行代码分割
  2. 修改manualChunks配置进行模块分割
  3. 修改单个文件大小限制(指标不治本因此本文不介绍)

二、解决方案

1. 动态导入

使用动态导入进行代码分割的好处在于实现了按需加载 ,如首页加载时不再需要加载其它页面中尚未涉及到的资源文件,而是只有当组件实际渲染时,对应的代码才会被加载,从而优化加载性能。

1.1 基于组件的动态导入 ------ React 异步组件

将包含大量依赖的复杂组件进行拆分能在 React 中基于组件的动态导入是通过异步组件来实现的,具体例子可以看下面的代码:

tsx 复制代码
export default function PostDetailPage() {
    const PostDetail = React.lazy(() => import("@/components/PostDetail"));
    return (
        <React.Suspense fallback={<Loading/>}>
            <PostDetail/>
        </React.Suspense>
    );
}

其中lazy用于动态导入,需要传入一个工厂方法,该方法返回一个Promise,而import()是 ES Module 中提供的用于动态导入模块的方法,其同样返回一个Promise

异步组件渲染需要通过Suspense组件来实现,children自然是传入使用lazy方法动态导入的异步组件,通过fallback还可以指定异步加载过程中需要展示的内容,如加载动画等。

如果组件内部还需要异步加载数据,也可以将这一部分逻辑写在lazy中:

tsx 复制代码
export default function PostDetailPage() {
    const [data, setData] = useState();
    const PostDetail = React.lazy(async () => {
        const newData = await fetchData(); // 异步加载数据
        setData(newData);
        return await import("@/components/PostDetail");
    });
    return (
        <React.Suspense fallback={<Loading/>}>
            <PostDetail data={data}/>
        </React.Suspense>
    );
}

同样也可以借助 react-loadable 库来实现异步组件渲染:

tsx 复制代码
export default function PostDetailPage() {
    const LoadablePostDetail = Loadable.Map({
        loader: {
            component: () => import("@/components/PostDetail"),
            data: () => fetchData(),
        },
        loading() {
            return (
                <Loading/>
            );
        }
        render(loaded) {
            const PostDetail = loaded.component.default;
            const data = loaded.data;
            return (
                <PostDetail data={data}/>
            );
        },
    });
    return (
        <LoadablePostDetail/>
    );
}

其中loader可以指定多个异步加载数据源,loading指定异步加载过程中展示的内容,render渲染异步加载完成后的组件,异步数据源加载完成的结果将通过参数传入。

1.2 基于路由的动态导入(React Router v6)

基于组件的动态导入方法粒度小,能实现更灵活的代码分割,如果只是需要针对不同页面进行大粒度的代码分割,则可以采用基于路由的动态导入方法。基于 React Router v6 的代码示例如下:

tsx 复制代码
const router = createBrowserRouter([
    {
        path: "/:userId",
        async lazy() {
            const {default: PostPage} = await import("@/pages/PostPage");
            return {Component: PostPage};
        },
    },
    {
        path: "/:userId/post/:postId",
        async lazy() {
            const {default: PostDetailPage} = await import("@/pages/PostDetailPage");
            return {Component: PostDetailPage};
        },
    },
]);

export default function App() {
    return (
        <RouterProvider router={router}/>
    );
}

其实也是对 React 异步组件进行了一层封装而已。

再次打包看一下效果:

可以看到每个异步加载的组件都被打包成了单个文件,从而减少了文件的大小。

2. 自定义模块分割策略

若项目中包含大量依赖包,也可能导致打包产物单个文件过大,此时可以通过将依赖模块拆分到不同的文件来解决。Vite/Rollup 提供了 manualChunks 配置可以自定义模块分割策略。

ts 复制代码
// vite.config.ts

import {defineConfig} from "vite";

export default defineConfig({

    // 此处省略其它配置项
    
    build: {
        rollupOptions: {
            output: {
                manualChunks(id) {
                    if (id.includes("/node_modules/")) {
                        const module = id.split("/node_modules/")[1].split("/")[1];
                        const map: Record<string, string> = {
                            // "antd": "antd",
                            "@ant-design": "ant",
                            "@arco-design": "arco",
                            "ahooks": "ahooks",
                            "lodash": "lodash",
                            "d3": "d3",
                            "lucide": "lucide",
                        };
                        for (const key of Object.keys(map)) {
                            if (module.startsWith(key)) {
                                return map[key];
                            }
                        }
                    }
                },
            },
        },
    },
});

manualChunks可以指定一个方法,传入参数为拆分模块的完整路径(如下图所示),当方法返回一个字符串时该模块被打包到文件名以该字符串为开头的文件中,否则采用默认模块分割策略。

可以看到打包产物中@ant-design@arco-design等依赖模块被打包到了对应的单个文件中:

然而自定义模块拆分策略可能导致一些问题:

  1. 打包产物由于依赖缺失或循环依赖等问题无法正常运行
  2. 单个依赖模块本身过于庞大也会导致打包产物单个文件体积过大

这些问题我目前还没有找到较好的解决方案,后续解决后会继续进行分享。

三、引用与参考

代码分割:打包完产物体积太大,怎么拆包?_manualchunks-CSDN博客

项目vite1.0升级到2.0打包遇到Some chunks are larger问题如何解决_using dynamic import() to code-split the applicati-CSDN博客

相关推荐
September_ning34 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人44 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0011 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking3 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫4 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull8 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet20 小时前
那总结下来,react就是落后了
前端·react.js
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router