基于Rspack实现大仓应用构建提效实践|得物技术
目录
一、实践背景
二、业界方案
三、技术选型
四、方案设计思路
1. 方案难点
2. 通过扩展插件命令实现Rspack构建
3. 官方平替插件+少部分自研扩展支持原有插件能力
4. 基于配置映射实现业务超低接入成本
5. 方案架构
6. 稳定性保障
五、方案效益
六、分享过程中的一些干货
1. 支持ts/tsx
2.React HMR
3. Module Federation
4. 样式按需引入:babel-plugin-import
5. cssModules:auto-css-modules
6. 影响编译效率的devtool: "source-map"
7. 可能你不知道的低效代码
七、未来规划
八、特别说明
一
实践背景
基于原有Node.js语言实现,通过缓存等方案来提升构建效率,主要是缓存、预构建的方式来减少编译。此类方案多数存在条件限制,比如缓存方案前提是第一次先生成缓存来提升二次构建效率,对于发布平台等需要冷启场景无法生效。 另外一类是采用Golang、Rust等语言重新实现耗时较为复杂的编译过程,从语言层面实现编译过程的性能提升。比较有代表的有,基于Golang实现的esbuild、基于Rust实现的SWC,都在对应的场景得到不少的性能提升。
二
业界方案
三
技术选型
高性能:基于Rust实现核心能力,全量编译+增量编译(HMR)的实现方式,官方宣称实际落地有5~10倍的提升,随着未来逐步优化完善还有提升空间,且生产和开发阶段除缓存之外,基本可以获得一致性的性能收益。 低成本:Rspack大量兼容webpack生态,大量配置和插件可以直接或者调整一下配置即可复用,仅需对一些特殊的插件自研定制开发即可。以下是对项目主要使用到的webpack能力进行了梳理,并对其在Rspack中的情况情况也进行了对照。
四
方案设计思路
方案难点
UmiJS仅支持webpack和Vite两种构建模式,如何扩展Rspack构建? 业务应用中有大量使用UmiJS插件、Babel插件来实现一些特殊能力,如何支持此类插件的能力? 如何尽可能降低业务应用接入成本,进而达成方案快速应用到各个业务应用中?
通过扩展插件命令实现Rspack构建
官方平替插件+少部分自研扩展支持原有插件能力
基于配置映射实现业务超低接入成本
方案架构
通过扩展自研插件,提供自定义的rspack-dev和rspack-build命令来提供开发、生产模式,接入时仅需要安装插件并替换启动命令即可(举个例子:package.json中修改max dev为max rspack-dev)。 通过插件内部对配置进行转化,将原本UmiJS的配置转为Rspack配置,保障业务应用接入时基本不感知。另外在开发成本方面,由于大多数loader和plugin可以复用,主要是配置和loader等能力替换映射成本。 方案基于UmiJS的max扩展,原本UmiJS的扩展能力不受影响。业务高使用率Babel插件有现成的SWC扩展能力可直接替换(比如:Babel-plugin-import、svgr等),少部分自研插件需要使用Rust重写。
稳定性保障
webpack转Rspack:Rspack项目内平移了大量的webpack测试用例用于保障一致性,另外默认严格模式,出现不兼容配置会抛出错误中断构建,保证了基础方面的稳定性。
Babel(v7)转SWC:SWC支持所有stage 3 perposals 、preset-env,JS/TS语法编译能力上跟Babel 7对齐。在插件生态上不一致,若有使用Babel插件,需要考虑替换方案(详情参考前文中的Babel插件使用情况)。
构建报错中断策略:配置上出现不支持的Babel插件直接报错中断构建,避免未支持的内容被跳过进而导致异常发布上线。 阶段推进落地策略:由于大多数构建运行都是在开发测试阶段(粗略统计平台发布70%左右为测试环境),先行接入开发&测试环境达到构建效率提升,等开发测试阶段跑稳定之后,再从非核心应用开始试点上线,功能稳定之后再逐步推广。 极简的应急恢复策略:由于极低的接入成本,若接入遇到问题想快速回退也非常简单,仅需回退命令为dev/build即可完成应急恢复。
五
方案效益
添加并安装依赖:添加并安装@umijs/plugin-rspack依赖(得物私有npm包)。
dx add @umijs/plugin-rspack@latest -D
添加UmiJS的plugin:在config/config.ts中修改plugins属性。
{
// 原有其他配置
...
plugins: [
// 原有其他插件
...,
// 添加 @umijs/plugin-rspack 插件
'@umijs/plugin-rspack',
],
// 原有其他配置
...
}
修改构建命令:修改package.json中的构建命令,将对应环境的命令调整为rspack-dev/rspack-build,并增加NODE_ENV配置。
{
"scripts": {
// start 对应支持本地的dx dev,
// 原配置样例
- "start": "cross-env BUILD_ENV=dev max dev",
// 改rspack构建样例
+ "start": "cross-env BUILD_ENV=dev NODE_ENV=development max rspack-dev",
// pnpm:build:x 对应支持发布平台指定的环境
// 原t1配置样例
- "pnpm:build:t1": "cross-env BUILD_ENV=t1 max build",
// t1改rspack构建样例
+ "pnpm:build:t1": "cross-env BUILD_ENV=t1 NODE_ENV=production max rspack-build",
... // 原先的其他配置,酌情进行调整
}
}
六
分享过程中的一些干货
支持ts/tsx
export default {
module: {
rules: [
{
test: /\.(j|t)s(x)?$/,
loader: 'builtin:swc-loader',
options: {
env: {
// 浏览器兼容性,支持browserslist,详细可以参考:https://swc.rs/docs/configuration/supported-browsers#targets
targets: {
chrome: "80",
},
},
// js/ts编译配置
jsc: {
parser: {
// 开启ts编译
syntax: 'typescript',
// 开启tsx编译
tsx: true,
// 开启@装饰器编译
decorators: true,
// 动态import
// dynamicImport: false,
},
transform: {
// react运行环境配置
react: {
// dev模式打开development启动react的开发模式
development: isDev,
},
// stage 1 的旧版本class decorators
legacyDecorator: true,
// 支持 ts emitDecoratorMetadata
decoratorMetadata: true,
},
},
},
},
]
}
}
React HMR
直接使用Rspack的devServer情况下,需要同时开启devServer.hot和@rspack/plugin-react-refresh。如下配置:
import ReactRefreshPlugin from '@rspack/plugin-react-refresh';
export default {
... 其他配置
devServer: {
// 开启HMR,官方文档也有体现
hot: true,
},
plugins: [
...其他插件
// React热更新支持插件
isDev && new ReactRefreshPlugin(),
]
}
若不使用devServer的情况下,需要用rspack.HotModuleReplacementPlugin来实现devServer.hot的能力。由于这种方式未在实践过程中进应用,这里不进行具体的使用举例。
Module Federation
import { container } from '@rspack/core';
export default {
... 其他配置
plugins: [
new container.ModuleFederationPluginV1({
... 这里是mf的配置
})
]
}
样式按需引入:babel-plugin-import
// 手写源代码
import { Button, Input } from 'antd';
// 通过插件编译后
import { Button, Input } from 'antd';
import 'antd/lib/button/style';
import 'antd/lib/input/style';
export default {
module: {
rules: [
{
test: /\.(j|t)s(x)?$/,
loader: 'builtin:swc-loader',
options: {
experimental: {
// babel-plugin-import的配置
import: [
// pd按需注入样式
{ libraryName: 'poizon-design', libraryDirectory: 'es', style: true },
// antd按需注入样式
{ libraryName: 'antd', libraryDirectory: 'es', style: true },
],
}
}
}
]
}
}
cssModules:auto-css-modules
添加swc-plugin-auto-css-modules插件(实际上开源的插件并不能用,后文详细说明原因)。 添加builtins.css配置,指定cssModules编译时的方式,主要有样式名是否保持,输出的样式格式。 添加resourceQuery识别,并给对应的内容加type: 'css/module';Rspack默认会对type: 'css/module'的代码当做cssModules编译,这个用法会比使用css-loader+style-loader更方便。
const postcssLoader = {
loader: require.resolve('postcss-loader'),
options: {
// ...此处省略less-loader配置
}
};
const lessLoader = {
loader: require.resolve('less-loader'),
options: {
// ...此处省略less-loader配置
}
}
export default {
builtins: {
css: {
// cssModule默认配置
modules: {
// class保持原样输出
localsConvention: 'asIs',
// class转换后的格式,localIdentName跟css-loader并不完全兼容,比如[hash:base64:5]这种写法就会报错
localIdentName: '[local]_[hash:8]',
},
},
},
module: {
rules: [
{
test: /\.(j|t)s(x)?$/,
loader: 'builtin:swc-loader',
options: {
experimental: {
plugins: [
// ...此处其他配置内容
// 添加 swc-plugin-auto-css-modules 插件执行?modules的注入
[require.resolve('swc-plugin-auto-css-modules'), {}]
]
}
},
},
{
test: /\.css(\?.*)?$/,
oneOf: [
{
// 通过resourceQuery来匹配?modules
resourceQuery: /modules/,
use: [postcssLoader],
// type声明指定为cssModules解析
type: 'css/module'
},
{
use: [postcssLoader],
// type声明指定为普通css解析
type: 'css'
},
]
},
{
test: /\.less(\?.*)?$/,
oneOf: [
{
resourceQuery: /modules/,
use: [postcssLoader, lessLoader],
type: 'css/module'
},
{
use: [postcssLoader, lessLoader],
type: 'css'
},
]
},
]
}
}
Rspack使用的swc_core为0.88.x~0.89.x对应@swc/core为@swc/core@1.3.106~@swc/core@1.3.107(swc官网可见)。 swc-plugin-auto-css-modules的@1.6.0虽然在文档上写着是兼容>= 1.3.106版本,但实际上由于其内部使用了swc_core@0.90.13(详见Github源码)。 但swc在0.90.x进行了ast的重构,跟之前的版本有了较大出入,导致无法生成的wasm调用无法兼容。
将swc-plugin-auto-css-modules的swc_core修改版本后,在本地构建生成一个wasm文件,并放到项目内。 swc的plugin修改为引入本地构建的wasm文件,其配置如下。
plugins: [
// ...此处其他配置内容
// 添加 swc-plugin-auto-css-modules 插件执行?modules的注入
// 删掉原有的 swc-plugin-auto-css-modules 引入
- [require.resolve('swc-plugin-auto-css-modules'), {}]
// 修改为引入项目相对目录下的swc_plugin_auto_css_modules.wasm(因为开源的发版有发版周期,先进行自编译使用)
+ [path.join(__dirname, '../../swc-plugins/swc_plugin_auto_css_modules.wasm'), {}],
]
影响编译效率的devtool: "source-map"
devtool: source-map(40.82s)
devtool: cheap-module-source-map(39.46s)
devtool: eval(32.94s)
devtool: false (31.21s)
可能你不知道的低效代码
📢📢📢:建议平时项目中排查注意一下,可以将此类代码进行删除,一个小小的习惯就可以直接提升不小的构建和访问性能。
七
未来规划
八
特别说明
相关参考资料站点:
Rspack:https://www.rspack.dev/
UmiJS: https://umijs.org/
Webpack:https://webpack.js.org/
SWC:https://swc.rs/
Vite:https://cn.vitejs.dev/guide/
Turbopack:https://turbo.build/pack
往期回顾
文 / 期越
关注得物技术,每周一、三、五更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: