Migrating from Webpack to Rspack
This guide documents the process of migrating from Webpack to Rspack using Shakapacker 9+. Rspack is a high-performance bundler written in Rust that provides significantly faster builds (~20x improvement) while maintaining Webpack API compatibility.
Prerequisites
- Shakapacker 9.0+ (Rspack support was added in 9.0)
- Node.js 20+
- React on Rails 13+
Quick Start
For new projects or simple migrations, the generator handles most of the setup:
# Generate with Rspack from scratch
rails generate react_on_rails:install --rspack
# Or switch an existing app
bin/switch-bundler rspackFor complex projects with SSR, CSS Modules, or custom configurations, continue reading for important considerations.
Breaking Changes in Shakapacker 9
CSS Modules: Named vs Default Exports
This is the most critical breaking change. Shakapacker 9 changed the default CSS Modules configuration from default exports to named exports (namedExport: true).
Symptoms:
- CSS modules returning
undefined - SSR errors:
Cannot read properties of undefined (reading 'className') - Build warnings:
export 'default' (imported as 'css') was not found
Affected code pattern:
// This pattern breaks with Shakapacker 9 defaults
import css from './Component.module.scss';
console.log(css.myClass); // undefined!Solution: Configure CSS loader to use default exports in your webpack configuration:
// config/webpack/commonWebpackConfig.js
const { generateWebpackConfig, merge } = require('shakapacker');
const commonWebpackConfig = () => {
const baseWebpackConfig = generateWebpackConfig();
// Fix CSS modules to use default exports for backward compatibility
baseWebpackConfig.module.rules.forEach((rule) => {
if (rule.use && Array.isArray(rule.use)) {
const cssLoader = rule.use.find((loader) => {
const loaderName = typeof loader === 'string' ? loader : loader?.loader;
return loaderName?.includes('css-loader');
});
if (cssLoader?.options?.modules) {
cssLoader.options.modules.namedExport = false;
cssLoader.options.modules.exportLocalsConvention = 'camelCase';
}
}
});
return baseWebpackConfig;
};
module.exports = commonWebpackConfig;Key insight: This configuration must be inside the function so it applies to a fresh config each time.
Rspack-Specific Configuration
Bundler Auto-Detection Pattern
Use conditional logic to support both Webpack and Rspack in the same configuration files:
// config/webpack/commonWebpackConfig.js
const { config } = require('shakapacker');
// Auto-detect bundler from shakapacker config
const bundler = config.assets_bundler === 'rspack' ? require('@rspack/core') : require('webpack');
// Use for plugins that need the bundler reference
clientConfig.plugins.push(
new bundler.ProvidePlugin({
/* ... */
}),
);
serverConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));Benefits:
- Single codebase for both bundlers
- Easy to compare configurations
- Clear visibility of bundler-specific differences
Server Bundle: CSS Extract Plugin Filtering
Rspack uses a different CSS extract loader path than Webpack. Server-side rendering configs that filter out CSS extraction must handle both:
// config/webpack/serverWebpackConfig.js
const configureServer = (serverWebpackConfig) => {
serverWebpackConfig.module.rules.forEach((rule) => {
if (rule.use && Array.isArray(rule.use)) {
// Filter out CSS extraction loaders for SSR
rule.use = rule.use.filter((item) => {
let testValue;
if (typeof item === 'string') {
testValue = item;
} else if (typeof item.loader === 'string') {
testValue = item.loader;
}
// Handle both Webpack and Rspack CSS extract loaders
return !(
testValue?.match(/mini-css-extract-plugin/) ||
testValue?.includes('cssExtractLoader') || // Rspack uses this path
testValue === 'style-loader'
);
});
}
});
};Why this matters: Rspack uses @rspack/core/dist/cssExtractLoader.js instead of Webpack's mini-css-extract-plugin. Without this fix, CSS extraction remains in the server bundle, causing intermittent SSR failures.
Server Bundle: Preserve CSS Modules Configuration
When configuring SSR, merge CSS modules options instead of replacing them:
// ❌ Wrong - overwrites namedExport setting
if (cssLoader && cssLoader.options) {
cssLoader.options.modules = { exportOnlyLocals: true };
}
// ✅ Correct - preserves existing settings
if (cssLoader && cssLoader.options && cssLoader.options.modules) {
cssLoader.options.modules = {
...cssLoader.options.modules, // Preserve namedExport: false
exportOnlyLocals: true,
};
}React Runtime Configuration
SWC React Runtime for SSR
If using SWC (common with Rspack), you may need to use the classic React runtime for SSR compatibility:
// config/swc.config.js
const customConfig = {
options: {
jsc: {
transform: {
react: {
runtime: 'classic', // Use 'classic' instead of 'automatic' for SSR
refresh: env.isDevelopment && env.runningWebpackDevServer,
},
},
},
},
};Symptom: SSR error about invalid renderToString call or function signature detection issues.
Additional Configuration
ReScript Support
If using ReScript, add .bs.js to resolve extensions:
// config/webpack/commonWebpackConfig.js
const commonOptions = {
resolve: {
extensions: ['.css', '.ts', '.tsx', '.bs.js'],
},
};Development Hot Reloading
Different plugins are required for hot reloading:
// config/webpack/development.js
const { config } = require('shakapacker');
if (config.assets_bundler === 'rspack') {
const ReactRefreshPlugin = require('@rspack/plugin-react-refresh');
clientWebpackConfig.plugins.push(new ReactRefreshPlugin());
} else {
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
clientWebpackConfig.plugins.push(new ReactRefreshWebpackPlugin());
}Shakapacker Configuration
Enable Rspack in config/shakapacker.yml:
default: &default
assets_bundler: 'rspack' # or 'webpack'
webpack_loader: 'swc' # Rspack works best with SWCTroubleshooting
CSS Modules Return undefined
Symptoms:
css.classNameisundefined- SSR crashes with property access errors
- Works in development but fails in SSR
Solutions:
- Configure
namedExport: false(see Breaking Changes section) - Ensure server config preserves CSS modules settings
- Filter Rspack's
cssExtractLoaderfrom server bundle
Intermittent SSR Failures
Cause: Incomplete CSS extraction filtering in server config.
Solution: Update the CSS extract loader filter to include cssExtractLoader for Rspack (see Server Bundle section).
Module Resolution Errors
Symptom: Module not found: Can't resolve './file.bs.js'
Solution: Add the file extension to webpack's resolve.extensions configuration.
Third-Party Package Issues
Some packages may not ship compiled files. Use patch-package to fix:
pnpm add --save-dev patch-package postinstall-postinstallAdd to package.json:
{
"scripts": {
"postinstall": "patch-package"
}
}Performance Benefits
After migration, expect:
- Build times: ~53-270ms with Rspack (vs seconds with Webpack)
- ~20x faster transpilation with SWC
- Faster CI runs and development iteration
Reference Implementation
For a complete working example, see the react-webpack-rails-tutorial Rspack migration PR.
Related Documentation
- Webpack Configuration - Rspack vs Webpack overview
- Server-Side Rendering - SSR configuration
- Troubleshooting Guide - General troubleshooting