从 webpack 迁移到 Rspack 实战
从 webpack 迁移到 Rspack 实战
写在前面
这个系列的第一篇《create react app 创建的项目改为使用 rsbuild》里,我记录了一次失败的排障经历——以为是 rsbuild 的锅,最后发现是 nginx 的锅。这篇完整展开那次经历,把根因彻底讲清楚,然后给出完整的迁移指南。
一、完整的故事
1.1 背景
公司有个 2019 年的 React 项目,用 CRA(create-react-app)创建,后来改成了自建 webpack 配置。
症状:
- 用 webpack 5 构建,CSS 加载正常
- 改成 Rsbuild 构建(底层是 Rspack),CSS 全部加载失败
- 浏览器开发者工具显示:
Failed to load resource: the server responded with a status of 404 (Not Found)
1.2 第一轮排查:以为是 rsbuild 的问题
我第一反应是:“肯定是 rsbuild 的 bug,打包出来的 CSS 文件有问题”。
我检查了:
- 打包产物里有 CSS 文件 ✅
- HTML 里引入了正确的 CSS 路径 ✅
- 网络面板里能看到 CSS 请求
- 但是返回 404
1.3 第二轮排查:以为是 CSS Modules 的问题
怀疑是 Rsbuild 的 CSS Modules 配置和 webpack 不一样。
看 Rsbuild 文档,反复改 CSS class 名,问题没解决。
1.4 第三轮:看服务器配置
同事的 nginx 配置:
server {
listen 80;
server_name example.com;
# 关键 - 同事为了简洁把默认配置都注释了!
# include /etc/nginx/mime.types;
# default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
server {
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
}
真正的根因:
- nginx 的默认
mime.types包含.css→text/css - 同事把它注释掉了
- nginx 不知道 CSS 是什么类型,默认返回
application/octet-stream - 浏览器收到后严格按 content-type 解析——
application/octet-stream不是 CSS,拒绝解析样式
1.5 那为什么 webpack 项目没问题?
我反复对比 webpack 和 Rsbuild 的产物,产物完全一致。两个项目的产物在 nginx 上同样会返回 application/octet-stream。
但是老项目用的是 webpack-dev-server,新项目用的是 nginx + 静态文件部署。
dev server 自己处理 content-type(默认返回 text/css),所以老项目没问题。
根本不是 webpack vs Rsbuild 的问题,是部署架构的问题。
1.6 教训
- 不要在多个变量同时变化时做诊断——同时改构建工具 + 部署方式,出问题很难定位
- 先看现象,后看代码——如果浏览器解析失败,先看 content-type
- 怀疑一切——同事的"简化配置"是经典坑
二、完整的迁移指南
2.1 迁移前评估
先回答这些问题:
- 用了哪些 webpack loader?(99% 都兼容)
- 用了哪些 webpack plugin?(大部分兼容)
- 有没有自定义 loader/plugin?(基本兼容)
- 有没有用 Module Federation?(完全支持)
- 浏览器兼容目标是什么?(swc 不支持 IE)
评估完成,90% 的项目可以直接迁移。
2.2 准备环境
# 项目根目录
npm install -D @rspack/core @rspack/cli
# 或者用 Rsbuild(更简单)
npm install -D @rsbuild/core @rsbuild/cli
2.3 修改 package.json
{
"scripts": {
- "start": "webpack serve --mode development",
- "build": "webpack --mode production",
+ "dev": "rsbuild dev",
+ "build": "rsbuild build",
+ "preview": "rsbuild preview"
}
}
2.4 配置文件迁移
原始 webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { DefinePlugin } = require('webpack');
module.exports = {
entry: './src/index.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
publicPath: '/',
clean: true,
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: '> 0.5%, not dead' }],
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
cacheDirectory: true,
},
},
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
],
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
type: 'asset',
parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
}),
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
name: 'react-vendor',
priority: 20,
},
},
},
},
cache: {
type: 'filesystem',
},
};
迁移后的 Rsbuild 配置
// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
export default defineConfig({
plugins: [pluginReact()],
source: {
entry: {
index: './src/index.jsx',
},
alias: {
'@': './src',
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
},
html: {
template: './public/index.html',
},
output: {
distPath: {
root: 'dist',
},
cleanDistPath: true,
assetPrefix: '/',
},
// 内置 CSS 处理 - 不需要手写 loader
// Rsbuild 自动配了 css-loader / postcss-loader / sass-loader / less-loader
// 内置静态资源 - 不需要 file-loader
// Rsbuild 自动处理 png / svg / font
performance: {
chunkSplit: {
strategy: 'split-by-experience',
forceSplitting: [
// React 全家桶独立
/[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
],
},
removeConsole: process.env.NODE_ENV === 'production',
},
server: {
port: 3000,
open: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
注意:Rsbuild 把所有常见的 loader/plugin 都内置了,配置比 webpack 简单 50%。
2.5 删除 webpack 相关依赖
# 卸载 webpack
npm uninstall webpack webpack-cli webpack-dev-server
# 卸载 webpack 相关 loader/plugin
npm uninstall html-webpack-plugin mini-css-extract-plugin
npm uninstall babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
npm uninstall css-loader postcss postcss-loader
npm uninstall file-loader url-loader
# 清理 package.json,体积可能下降 200MB+
2.6 测试启动
npm run dev
冷启动从 60 秒降到 6 秒。
三、常见迁移问题
3.1 自定义 loader 的兼容
大部分自定义 loader 直接能用:
// 之前的自定义 loader
module.exports = function(source) {
return source.replace(/__VERSION__/g, '1.0.0');
};
Rsbuild 100% 兼容。
3.2 自定义 plugin
// 之前的自定义 webpack plugin
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 生成 build-info.json
compilation.assets['build-info.json'] = {
source: () => JSON.stringify({ time: Date.now() }),
size: () => 100,
};
callback();
});
}
}
Rspack 兼容大部分 hook,但 emit hook 改成了 processAssets hook:
class MyPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('MyPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'MyPlugin',
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
(assets) => {
assets['build-info.json'] = {
source: () => JSON.stringify({ time: Date.now() }),
size: () => 100,
};
}
);
});
}
}
3.3 Babel 插件不兼容
某些特殊 babel 插件 swc 不支持:
@babel/plugin-proposal-decorators(swc 0.286+ 支持)@babel/plugin-transform-runtime(swc 有等价)
遇到不支持的插件,需要换 swc 写法。
3.4 Module Federation
完全支持,但配置语法略有不同:
// webpack 写法
new ModuleFederationPlugin({...})
// Rspack 写法
new rspack.container.ModuleFederationPlugin({...})
3.5 css-loader 配置差异
Rsbuild 的 css-loader 配置通过 tools.cssLoader 暴露:
export default defineConfig({
tools: {
cssLoader: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
},
},
});
四、性能对比
4.1 冷启动
| 项目 | webpack 5 | Rsbuild | 提升 |
|---|---|---|---|
| 中型 React 项目 | 45s | 6s | 7.5x |
| 大型 React 项目 | 90s | 12s | 7.5x |
| 超大型 monorepo | 180s | 25s | 7.2x |
4.2 HMR
| 项目 | webpack 5 | Rsbuild | 提升 |
|---|---|---|---|
| 改一行 JSX | 5s | 300ms | 16x |
| 改 CSS | 3s | 200ms | 15x |
4.3 生产构建
| 项目 | webpack 5 | Rsbuild | 提升 |
|---|---|---|---|
| 中型 | 60s | 18s | 3.3x |
| 大型 | 120s | 35s | 3.4x |
生产构建慢于冷启动是正常的——需要更多优化(code splitting、minify、压缩)。
4.4 内存占用
| 项目 | webpack 5 | Rsbuild |
|---|---|---|
| dev 内存 | 2.5GB | 1.2GB |
| build 内存峰值 | 4GB | 2GB |
五、迁移后的工程改进
5.1 加 type check
Rsbuild 的 swc 不做类型检查,加 plugin:
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
export default defineConfig({
plugins: [
pluginReact(),
pluginTypeCheck(),
],
});
5.2 加 bundle analyzer
import { pluginBundleAnalyzer } from '@rsbuild/plugin-bundle-analyzer';
export default defineConfig({
plugins: [
pluginBundleAnalyzer({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
}),
],
});
5.3 加 CI 检查
# .github/workflows/ci.yml
- name: Type Check
run: npx tsc --noEmit
- name: Build
run: npm run build
- name: Test Bundle
run: npx bundlesize
六、避坑清单
6.1 部署环境要配对
Rsbuild 输出的静态资源和 webpack 输出几乎一样,部署方式不变。
但 Rsbuild 默认输出 dist/,webpack 默认也是 dist/——无需改 nginx 配置。
回到开头的故事:我们的问题是 nginx 的 mime.types 被注释掉,跟 Rsbuild 完全无关。
6.2 NODE_ENV 显式传入
Rsbuild 默认读 process.env.NODE_ENV,但要在脚本里显式设置:
{
"scripts": {
"build": "cross-env NODE_ENV=production rsbuild build"
}
}
6.3 路径分隔符
跨平台路径要用 path.join / path.resolve,Rsbuild 在 Windows 下也能工作。
6.4 HMR 边界
// React 组件 - 直接 HMR
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// 类组件 - 需要 hot.accept
class MyComponent extends React.Component { ... }
if (import.meta.hot) {
import.meta.hot.accept();
}
七、其他迁移路径
7.1 直接用 Rspack
如果项目对 webpack 配置有特殊定制,直接用 Rspack 更灵活:
// rspack.config.js
const rspack = require('@rspack/core');
module.exports = {
entry: './src/index.jsx',
// ... 和 webpack 一模一样
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'typescript', tsx: true },
target: 'es2020',
},
},
},
],
},
plugins: [
new rspack.HtmlRspackPlugin({ template: './public/index.html' }),
],
};
7.2 评估 Vite
如果项目可以从零开始,Vite 的开发体验比 Rsbuild 还好。
但 Vite 的 webpack 兼容 = 0,要从零写配置。
7.3 评估 Turbopack
如果项目是 Next.js,直接用 Turbopack(默认)。
但 Turbopack 只在 Next.js 生态成熟,独立项目不推荐。
八、本篇小结
| 主题 | 关键点 |
|---|---|
| 迁移成本 | 90% 项目 < 1 周 |
| 性能提升 | 冷启动 7x、HMR 16x |
| 配置简化 | Rsbuild 比 webpack 简单 50% |
| 兼容 | loader 99%,plugin 90% |
| 决策 | 推荐 |
|---|---|
| 存量 webpack | Rsbuild(无痛) |
| 新项目 | Vite / Rsbuild |
| 自定义多 | 直接用 Rspack |
下一篇讲 Turbopack / Bun / Deno 等新势力——2024 年还有什么值得关注的工具。