我把大型团队项目从 vite 前端迁移到了 rsbuild,收益如何?

背景

我们团队的项目一直是使用 vite 作为打包工具的,由于 vite 开发环境和生产环境的打包策略不同(开发环境按需打包,生产环境全量打包),所以存在开发环境和线上环境打包产物表现不一致的风险。其中 css 样式不一致的问题尤为容易发生,目前在需求开发过程中,已多次遇到本地开发环境 css 样式符合 UI 稿,发布到测试环境或者线上之后就出现了差异的场景。

基于上述背景,我们期望迁移至一款开发和生产环境打包产物一致的工具。

方案调研

除上述背景外,我们期望的打包工具还应该满足以下特性:

  • 开发和生产环境打包产物一致
  • 打包性能优异
  • 生态良好、项目足够稳定
  • 迁移成本较低

要满足打包性能优异的特性,除 vite 外,我们的打包工具范围最终锁定在由 rust 开发项目内:

Turbopack Farm Rsbuild(基于rspack)
产物一致性 ❌ 不一致 ✅ 一致 ✅ 一致
打包性能 ✅ 快 ✅ 快 ✅ 快
生态及稳定性 ❌ 全新生态,生态链较差 ✅ 兼容 vite 插件(先试验了下 farm,遇到的坑太多了且解决不了,根据 github 的用户数以及 npm 下载量,感觉目前尚未达到稳定可用状态) ✅ 兼容 webpack 插件且已有众多线上项目验证
迁移成本 ❌ 高(没有对应的插件) ❌ 高(踩坑太多且无解决思路,应该是farm本身的坑) ✅ 中

经过调研和实践,最终选定 rsbuild 作为最终的工具。

最终收益

  • 打包时长:300s 左右 > 60s 左右,性能提升大概 400% 左右

    • 迁移前 vite 打包时长:292s
    • 迁移后 webpack 打包时长:66s
  • 流水线构建时长:360s 左右 > 130s 左右,减少 150%(整条流水线上除了打包之外,还有拉代码仓库、代码质量检测、安装依赖、上传 CDN 等其他工作,而构建工具只是减少了打包这一阶段的工作时长,但打包依然是占最大头的,所以收益还是很明显)

  • 冷启动时长:

    • 首页面渲染:10s 左右 > 25s 左右(这里指的是从执行 npm run dev 到页面渲染出内容的时长)
    • 其他页面首次切换:5s 左右 > 1s 内(vite 由于资源是按需加载,在加载其他页面时需要对新页面资源进行 esbuild 构建,所以会慢)

    可以看到首屏冷启动时长性能有比较大的劣化,这是因为 vite 是根据页面按需资源打包,而 rsbuild 是全量打包。(但全量打包目前应该是解决开发和生产产物不一致的必要条件,为解决我们的背景问题必不可少)

    冷启动时长综合对比,如果在开发过程中切换页面比较多的话,最终的等待时长 rspack 会胜于 vite;如果只关注于一个页面的开发,vite 这方面占优

  • 热更新:0.5s 内 > 1.5s 内(都是很低的数值,体感没有差别)

  • 资源分包数:以我们项目的首屏为例,可以看到 rsbuild 的分包策略要由于 vite(打包总体积几乎无变化的情况下,分包数少了很多):

    • css 文件数: 38项 > 9 项
    • js 文件数:81项 > 55 项

迁移过程及踩坑

安装相关依赖

安装 rsbuild 核心包:

sql 复制代码
yarn add @rsbuild/core @rsbuild/plugin-babel @rsbuild/plugin-less @rsbuild/plugin-sass @rsbuild/plugin-vue @rsbuild/plugin-vue-jsx -D

其他插件根据自己项目的需要进行安装,可以在 rsbuild 官网找到。常用的有:

less 复制代码
@rsbuild/plugin-babel
@rsbuild/plugin-less
@rsbuild/plugin-sass
@rsbuild/plugin-vue
@rsbuild/plugin-vue-jsx

更改打包配置文件

下面是以我们的项目为例(一些敏感代码没有在上面展示),将 vite.config.ts 迁移到 rsbuild.config.ts ,可以进行参考:

  • vite.config.ts:

    ts 复制代码
    import { defineConfig, loadEnv, UserConfigExport } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import vueJsx from '@vitejs/plugin-vue-jsx';
    import Inspect from 'vite-plugin-inspect';
    import AutoImport from 'unplugin-auto-import/vite';
    import { codeInspectorPlugin } from 'code-inspector-plugin';
    import path from 'path';
    
    const isProduction = env => env === 'production';
    const cdn = 'https://xxx.com/xyz/';
    
    export default ({ mode }: { mode: string }): UserConfigExport =>
        defineConfig({
            base: isProduction(mode) ? cdn : '/',
            plugins: [
                vue(),
                vueJsx(),
                codeInspectorPlugin({
                    bundler: 'vite',
                }), // 开发环境 Meta + shift 切换开启/关闭,点击 dom 跳转源代码
                AutoImport({
                    imports: ['vue', 'vue-router'],
                    dts: 'src/auto-import.d.ts',
                }),
            ],
            resolve: {
                alias: [
                    {
                        find: '@',
                        replacement: path.resolve(__dirname, './src'),
                    },
                ],
            },
            server: {
                host: '0.0.0.0',
                port: 4000,
                open: true,
                cors: true,
                proxy: {
                    '/api/': {
                        target: loadEnv(mode, process.cwd()).VITE_APP_HOST, // 转发域名
                        changeOrigin: true,
                        headers: {
                            referer: loadEnv(mode, process.cwd()).VITE_APP_REFERER,
                            host: loadEnv(mode, process.cwd()).VITE_APP_HOST,
                            Cookie: loadEnv(mode, process.cwd()).VITE_APP_COOKIE, // 转发
                        },
                    },
                },
            }
        });
  • rsbuild.config.ts:

    ts 复制代码
    import { defineConfig } from '@rsbuild/core';
    import { pluginVue } from '@rsbuild/plugin-vue';
    import { pluginVueJsx } from '@rsbuild/plugin-vue-jsx';
    import { pluginBabel } from '@rsbuild/plugin-babel';
    import { pluginLess } from '@rsbuild/plugin-less';
    import AutoImport from 'unplugin-auto-import/rspack';
    import { codeInspectorPlugin } from 'code-inspector-plugin';
    import path from 'path';
    
    
    const isProduction = env => env === 'production';
    const cdn = 'https://xxxx.com/xyz/';
    
    const { VITE_APP_HOST, VITE_APP_COOKIE, VITE_APP_REFERER } = import.meta.env;
    
    export default defineConfig(({ env }) => ({
        output: {
            assetPrefix: isProduction(env) ? cdn : '/',
            distPath: {
                root: 'dist',
                html: './',
                js: './',
                jsAsync: './',
                css: 'assets',
                cssAsync: 'assets',
                svg: 'assets',
                font: 'assets',
                image: 'assets',
                media: 'assets',
            },
            sourceMap: {
                js: isProduction(env) ? 'source-map' : 'cheap-module-source-map',
                css: true,
            },
        },
        plugins: [
            pluginLess({
                lessLoaderOptions: {
                    lessOptions: {
                        javascriptEnabled: false,
                    },
                    implementation: require.resolve('less'),
                },
            }),
            pluginBabel({
                include: /.(?:jsx|tsx)$/,
            }),
            pluginVue(),
            pluginVueJsx(),
        ],
        tools: {
            htmlPlugin: {
                template: './index.html',
            },
            rspack: {
                plugins: [
                    AutoImport({
                        imports: ['vue', 'vue-router'],
                        dts: 'src/auto-import.d.ts',
                    }),
                    codeInspectorPlugin({
                        bundler: 'rspack',
                    }),
                ],
                module: {
                    rules: [
                        {
                            resourceQuery: /raw/,
                            type: 'asset/source',
                        },
                    ],
                },
            },
        },
        source: {
            alias: {
                '@': path.resolve(__dirname, './src'),
            },
            define: {
                'import.meta.env.VITE_APP_HOST': `'${VITE_APP_HOST}'`,
            },
        },
        server: {
            host: '0.0.0.0',
            port: 4000,
            open: true,
            cors: true,
            proxy: {
                '/api/': {
                    target: loadEnv(mode, process.cwd()).VITE_APP_HOST, // 转发域名
                    changeOrigin: true,
                    headers: {
                        referer: loadEnv(mode, process.cwd()).VITE_APP_REFERER,
                        host: loadEnv(mode, process.cwd()).VITE_APP_HOST,
                        Cookie: loadEnv(mode, process.cwd()).VITE_APP_COOKIE, // 转发
                    },
                },
            },
        },
    }));

修改对应指令

json 复制代码
// package.json
{
  "scripts": {
    "dev": "rsbuild dev",
    "build": "rsbuild build",
    "preview": "rsbuild preview",
    // others...
  },
  // ...others
}

修改 index.html

移除掉 index.html 之前的 <script src="main.ts"></script> 引入代码。

踩坑

import.meta 不兼容

import.meta 是浏览器 module 模块中独有的语法,rsbuild 在 rsbuild.config.js 中可以使用 import.meta,但是在项目源代码中并不支持,所以我们需要对原本代码中的 import.meta 进行适配。

以我们项目为例,大部分代码中的 import.meta.XXX 都是在本地的 .env.local 文件中定义的,我们可以借助 source.define 去进行全局的声明:

ts 复制代码
import { defineConfig } from '@rsbuild/core';

const { VITE_APP_HOST } = import.meta.env;

export default defineConfig(({ env }) => ({
    source: {
        define: {
            'import.meta.env.VITE_APP_HOST': `'${VITE_APP_HOST}'`,
        },
    },
}));

另外我们项目还用到了 import.meta.url 去引入资源,这种需要改为 import 方式去引入:

javascript 复制代码
// 改之前:
const href = new URL('../assets/icon.png', import.meta.url).href;

// 改之后:
import IconPNG from '../assets/icon.png';
const href = IconPNG;

:deep 和 & 兼容问题

目前已知 @rsbuild/plugin-less 插件对于在 :deep 内部使用 less 的 & 符号会有问题,例如:

  • case1: 生效
less 复制代码
.container {
    .content {
        &_title {
            font-weight: bold;
        }
    }
}
  • case2: 生效
less 复制代码
.container {
    :deep(.content) {
        .content_title {
            font-weight: bold;
        }
    }
}
  • case3: 不生效
less 复制代码
.container {
    :deep(.content) {
        &_title {
            font-weight: bold;
        }
    }
}

后面会去 github 提个 issue,我们项目内部这种使用情况很少,所以暂时手动修改为不使用 &

css 中使用 v-bind 不支持三元运算符

不支持在 css 中 v-bind 使用三元运算符,可以改成在 js 中使用三元运算符替代

less 复制代码
// 改之前:
.container {
  height: v-bind(title ? '24px' : '48px');
}

// 改之后:
const containerHeight = title ? '24px' : '48px'; // js中写
.container {
  height: v-bind(containerHeight);
}

结论

我们的项目目前有 2500+ 文件、30w+ 行代码,算是一个很大的项目了。对于这样一个大型项目而言,从 vite 迁移 rsbuild 的过程中遇到的坑可以算很少了,而且迁移后的收益很明显,足以证明 rsbuild 及 rspack 是一个十分优秀的项目,且完全可以在生产环境中去应用了。

对于 webpack 构建的项目,我相信收益会更加明显,有折腾想法的朋友可以年底迁移一波作为 KPI 试试

最后的最后,快手基础平台团队招前端,欢迎在平台私信我投简历~

相关推荐
hackeroink33 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css