NOTE: This post was published while Rspack’s latest version was v1.0.0-beta.4. Some details are likely to change as Rspack approaches its 1.0 release.
Rspack is a Rust-based alternative to Webpack that promises to be faster and includes a few common conveniences too. After a year of trying, I’ve finally finished converting my two largest Webpack projects to Rspack. Here are some of the things I learnt along the way.
Why Rspack?
First, however, why choose Rspack? For me it’s because I’m already using Webpack for my two largest projects and Rspack offers a comparatively simple and low-risk upgrade compared to other build tools. The build times for these projects are also slow enough that speeding them up could make a worthwhile difference to productivity and CI costs.
But while we’re switching build tools, why not switch to Vite? A lot of people have switched from Webpack to Vite and seem to love it. The reasons I decided to go with Rspack for these particular projects are:
- Coming from Webpack, it’s a much simpler and less risky upgrade path.
- From what I can tell, Vite’s on-demand file serving appears to shine in SSR contexts like Next.js apps but I’m using Webpack for an SPA and a Web extension where this matters less.
- My experience with Vite so far hasn’t been great.
- I like to keep dev and prod builds as close as possible so I can confidently test what I’m shipping. Vite is weak in this area with its esbuild-based dev mode and Rollup-based prod builds. In fact, I found the gap so large that I gave up implementing some features in dev mode with Vite because it was too much effort to implement them twice.
- Rspack is apparently faster than Vite, at least in the areas that matter to me.
Perhaps the biggest problem with Rspack is that, like Webpack, it’s not easy to get started with and that appears to be why the Rspack team have also produced Rsbuild as a more approachable layer on top of Rspack.
Lessons learned along the way
The steps involved in migrating from Webpack are covered in Rspack’s Webpack migration guide so what follows are the things I learned that weren’t covered there or that might not be obvious.
TypeScript
Rspack uses SWC to transpile TypeScript which means
you no longer need ts-loader.
However, after building with Rspack,
I noticed that the generated JS assets were a lot bigger than when using Webpack
and ts-loader
.
It turned out that a lot of boilerplate was being generated
for features like async iterators.
In the Webpack build ts-loader
was picking up my tsconfig.json
which specified "target": "es2020"
so these features were not being downleveled.
In Rspack, however, SWC was downleveling them to ECMAScript 5.
The solution was to specify the
target
for SWC in the Rspack config.
For example:
const config = {
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'builtin:swc-loader',
/** @type {import('@rspack/core').SwcLoaderOptions} */
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es2022',
},
},
},
type: 'javascript/auto',
},
// ... similarly for tsx files
],
},
// ...
};
I believe it’s also possible to achieve this by specifying browser versions using
the env.targets
setting.
Even after doing that, however, the JS assets generated by Rspack
were still larger but when
1.0 alpha enabled optimization.concatenateModules
by default
they fell to within 3% of their Webpack counterparts
and were sometimes fractionally smaller.
Type checking
One consequence of using SWC to transpile TypeScript
is that type checking is no longer performed.
You’ll need to
enable isolatedModules
in your tsconfig.json
and work out when
and how you want to perform type checking.
I opted to use
fork-ts-checker-webpack-plugin
as
recommended by the official docs
but on reflection,
I wonder if that’s even necessary.
Assuming you have your editor set up to perform type checking
and you run tsc
in CI,
and perhaps as a pre-commit hook too
(e.g. using tsc-files
),
then you might not even need to perform type checking on each build.
Using Webpack I was ignoring certain TypeScript errors during development so that they didn’t interrupt me while refactoring like so:
use: {
loader: 'ts-loader',
options: env.ignoreUnused
? {
ignoreDiagnostics: [
6133 /* <variable> is declared but its value is never read */,
6192 /* All imports in import declaration are unused */,
],
}
: undefined,
}
With Rspack,
if you’re performing type checking using fork-ts-checker-webpack-plugin
,
you can pass in similar options there instead:
new ForkTsCheckerWebpackPlugin({
issue: {
exclude: env.ignoreUnused
? [
// <variable> is declared but its value is never read
{ code: 'TS6133' },
// All imports in import declaration are unused
{ code: 'TS6192' },
]
: [],
},
}),
CSS
Although the migration guide mentions replacing
mini-css-extract-plugin
with rspack.CssExtractRspackPlugin
,
the
documentation for CssExtractRspackPlugin
states:
If your project does not depend on css-loader, it is recommended to use the built-in CSS solution experiments.css of Rspack for better performance.
As a result, for my Web app project, I was able to update my CSS configuration from:
// rspack.config.js
const config = {
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
url: false,
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
},
],
},
// ...
],
},
// ...
};
To just:
const config = {
experiments: {
css: true,
},
module: {
rules: [
{
test: /\.css$/,
use: [{ loader: 'postcss-loader' }],
type: 'css/auto',
},
// ...
],
},
// ...
};
However, I noticed my CSS assets were ~11% larger
than those generated from Webpack.
Comparing the output,
I discovered that the Rspack output was not performing some optimizations
like converting rgba()
colors to hex values.
By default
Rspack uses Lightning CSS to minimize CSS assets
and it turns out the features Lightning CSS uses
as part of its default settings are very conservative.
After adding some more recent values to the
minimizerOptions.targets
property
Rspack’s output was slightly smaller than Webpack’s:
optimization: {
// ...
minimizer: [
new rspack.SwcJsMinimizerRspackPlugin({
// ...
}),
new rspack.LightningCssMinimizerRspackPlugin({
minimizerOptions: {
targets: [
'last 2 Chrome versions',
'Firefox ESR',
'last 2 Safari versions',
],
},
}),
],
},
Update (2024-08-17): It looks like the Rspack team have taken note of this and added default targets for Lightning CSS in beta 5!
Service workers
My app includes a Service Worker and relies on the
InjectManifest
plugin from Google’s Workbox
to trigger the generation of the Service Worker asset
and populate it with a precache manifest.
Unfortunately when I went to migrate my app,
this plugin wasn’t supported by Rspack.
Rspack 1.0.0-alpha.0, however,
added native support for recognizing Service Workers
(and various kinds of worklets too).
It’s a little bit quirky
in that you need to pass a URL
object to navigator.serviceWorker.register()
(not a string)
and you can’t pass a variable referring to a URL
object either.
For example, the following would cause a chunk to be generated for the service worker at sw.ts
:
const registrationPromise = navigator.serviceWorker.register(
new URL(
/* webpackChunkName: "serviceworker" */
'./sw.ts',
import.meta.url
)
);
You also need to take care to ensure the service worker chunk ends up with a fixed filename rather than one with a cache-busting hash in it:
// rspack.config.js
const config = {
// ...
output: {
chunkFilename: (assetInfo) => {
if (assetInfo.chunk?.name === 'serviceworker') {
return '[name].js';
}
return '[name].[contenthash].js';
},
},
};
However, all that still won’t help with embedding a precache asset manifest
like InjectManifest
does.
I tried
various alternatives
but none of them seemed to work with Rspack’s native service worker support
so I ended up
writing my own
by extracting just the needed parts of Workbox’s InjectManifest
plugin
and adapting them to Rspack.
Since then, Rspack have announced that Workbox is fully supported. My package is smaller and seems to work better than Workbox when making changes so I’ll stick with it for now but I’m sure most people will be happy to continue using Workbox as-is.
One quirk of the native Service Worker support, however, is that it appears to run before dead code elimination. As a result, if you are using build-time constants to disable service worker registration like in the following code, you may find the service worker asset is still generated even though it’s not referenced anywhere.
function registerServiceWorker() {
if (!('serviceWorker' in navigator) || !__ENABLE_SW__) {
return Promise.resolve(null);
}
// The following code will be eliminated when __ENABLE_SW__
// is falsy, but the sw.js asset will still be generated.
const registrationPromise = navigator.serviceWorker.register(
new URL(
/* webpackChunkName: "sw" */
'./sw.ts',
import.meta.url
)
);
// ...
return registrationPromise;
}
React Cosmos
Perhaps the biggest hurdle in migrating to Rspack was getting React Cosmos to play with it. I’m a big fan of React Cosmos for developing and testing components because I found that unlike Storybook it doesn’t require a lot of additional development dependencies but works with your existing bundler instead.
Unfortunately React Cosmos does not include support for Rspack, only Webpack, Vite, and a few others. It includes instructions on how to configure a custom bundler but I figured, “How hard can it be to port the Webpack plugin to Rspack?”
The answer, it turned out, was “quite hard”
mostly due to me insisting on building the plugin with
tsup and getting confused about
how each endpoint should be bundled.
A few weeks later, however,
react-cosmos-plugin-rspack
was born offering a drop-in replacement for the Webpack plugin.
Knip
The final dependency requiring attention was Knip. Knip is a tool for detecting unused cruft—dependencies, exports, files, etc.—in your project. If it sees you are using Webpack, it will look up your Webpack config and detect your project’s entrypoints, Webpack plugins etc. and analyze your project accordingly. Unfortunately, it didn’t know about Rspack.
My company is a sponsor of Lars’ work on Knip so I reached out to see if we could commission a Knip plugin for Rspack. Lars jumped on it and invited others to join in the crowdfunding and a few days later it was complete.
As a result, Knip detected that I no longer needed any of the following dependencies:
clean-webpack-plugin
(replaced byoutput.clean
)copy-webpack-plugin
(replaced byrspack.CopyRspackPlugin
)css-loader
(replaced byexperiments.css
)mini-css-extract-plugin
(replaced byexperiments.css
)react-cosmos-plugin-webpack
(replaced byreact-cosmos-plugin-rspack
)ts-loader
(replaced bybuiltin:swc-loader
andfork-ts-checker-webpack-plugin
)webpack
,webpack-cli
(duh)webpack-dev-server
(built into@rspack/cli
)workbox-webpack-plugin
(replaced by@birchill/inject-manifest-plugin
)
Command-line flags
Rspack CLI includes a built-in dev server but unfortunately,
the command-line flags differ from webpack-dev-server
in a few places,
notably missing options like --port
and --hot
/--no-hot
.
To handle such cases I ended up using various --env
values
and checking them in rspack.config.js
’s
configuration function.
This seems like a potentially unnecessary point of friction
that could be eased in a future release.
Update (2024-09-05): This has been fixed in version 1.0.1!
Rsdoctor
The Rspack team have helpfully created a plugin, Rsdoctor for analysing your bundle and its build times. For what it’s worth, I ended up with the following configuration:
new RsdoctorRspackPlugin({
disableTOSUpload: true,
linter: {
rules: {
// Don't warn about using non ES5 features
'ecma-version-check': 'off',
},
},
supports: { generateTileGraph: true },
});
What didn’t change
Apart from the above changes, everything else appeared to work as-is including the following plugins:
- html-webpack-inject-preload
- html-webpack-plugin
- Relative CI webpack plugin
- terser-webpack-plugin
- web-ext-webpack-plugin
- webpack-preprocessor
- webpack-utf8-bom
I also use the Bugsnag Webpack plugins but they only run on a new release so I haven’t had a chance to test them yet.
Results
So how much faster is Rspack after all?
For my SPA project I saw the following results:
Environment | Webpack | Rspack | Rspack without type checking |
---|---|---|---|
Home laptop | 19.1s | 8.1s (-57.7%) | 3.04s (-84.16%) |
Work desktop | 13.24s | 5.57s (-57.94%) | 2.44s (-81.61%) |
Since type checking is performed in a separate process, most of the time you’re only waiting on the build and the final column is what matters most.
Furthermore, if I am reading the results of Rsdoctor correctly,
it seems that most of the compile time comes from postcss-loader
so I hope that when
Tailwind 4 is released
I can switch to using
builtin:lightningcss-loader
instead and see further improvements to the compile time.
For my Web extension project, the improvements were even more significant, going from 11.65s to 3.6s (69% faster) on my desktop computer even with type checking enabled.
Update (2024-09-05): The Rspack team have added a list of
performance
bottlenecks.
In addition to postcss-loader
I am also using html-webpack-plugin
from that
list.
I currently need that in order to use html-webpack-inject-preload
(since
it checks for the HtmlWebpackPlugin
constructor
name).
If I can migrate that to work with
HtmlRspackPlugin
it
might improve the build time further.
Looking forward
As Rspack is approaching 1.0, we’ve had announcements for two more Rust-based build tools (which also appear to have roots in China) in Farm and Mako. Obviously there is also Turbopack, the Rust-based successor to Webpack, and Rolldown, the Rust-based version of Rollup.
It will be interesting to see which of these tools gets traction. Personally I think Rspack has a good chance of succeeding because:
-
Webpack is probably the most widely used build tool for apps today and Rspack provides the simplest and lowest risk migration path from Webpack. It’s easy to overlook this point but hopefully this post demonstrates how even for a modestly complex app like mine, migrating to a different build tool, even one that is largely compatible, can be quite involved.
-
Webpack has a rich plugin ecosystem, most of which work in Rspack. Many of the services I use like Relative CI and Bugsnag provide a Webpack plugin but don’t support any other build tools. Other build tools will be at an initial disadvantage unless they offer compatibility with Webpack’s plugin API.
-
Rspack is fast enough. Other tools might end up being fractionally faster (curiously both Mako and Farm claim to be marginally faster than Rsbuild but don’t show how they compare with Rspack) but if your build is already only 2~3 seconds, even an order of magnitude faster is not going to be a compelling enough reason to switch.
As Rspack approaches 1.0 I’m looking forward to seeing how it’s received and where the team takes it from here.