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博客

相关推荐
青青家的小灰灰3 小时前
React 架构进阶:自定义 Hooks 的高级设计模式与最佳实践
前端·react.js·前端框架
codingWhat11 小时前
前端组件库开发实践:从零到发布
前端·npm·vite
yuki_uix1 天前
Props、Context、EventBus、状态管理:组件通信方案选择指南
前端·javascript·react.js
牛奶1 天前
React 底层原理 & 新特性
前端·react.js·面试
牛奶1 天前
React 基础理论 & API 使用
前端·react.js·面试
小呆呆_小乌龟1 天前
同样是定义对象,为什么 TS 里有人用 interface,有人用 type?
前端·react.js
小岛前端1 天前
Cloudflare 掀桌子了,Next.js 迎来重大变化,尤雨溪都说酷!
前端·vite·next.js
代码小学僧1 天前
为什么我推荐前端项目都应该使用 TanStack Query 管理接口请求
前端·react.js·axios
不会敲代码11 天前
React 受控组件与非受控组件完全指南
前端·react.js
不会敲代码11 天前
React Hooks 进阶:useRef 核心用法与受控/非受控组件实战解析
前端·react.js·面试