Getting Started
    Core Concepts
    Building Features
    API Reference
    Deployment
    Upgrading
    Migrating
    Pro
    Advanced Topics
    Misc
    Configuration
    Guides
    Shakacode logoShakaCodeDeveloped by

    Copyright 2026 ShakaCode

    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 rspack

    For 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 SWC

    Troubleshooting

    CSS Modules Return undefined

    Symptoms:

    • css.className is undefined
    • SSR crashes with property access errors
    • Works in development but fails in SSR

    Solutions:

    1. Configure namedExport: false (see Breaking Changes section)
    2. Ensure server config preserves CSS modules settings
    3. Filter Rspack's cssExtractLoader from 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-postinstall

    Add 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.