Rspack Integration

August 29, 2026 ยท View on GitHub

Shakapacker supports Rspack as an alternative assets bundler to Webpack. Rspack is a fast Rust-based web bundler with webpack-compatible API that can significantly speed up your build times.

๐Ÿ“– For configuration options, see the Configuration Guide

Version Compatibility

Shakapacker supports Rspack v2 (^2.0.0) going forward. Rspack v1 is no longer a supported target for new Shakapacker releases.

Fresh installs use the supported Rspack v2 ranges from lib/install/package.json.

Rspack v2 note: Rspack v2 ships as a pure ESM package and requires Node.js 20.19.0+.

Why Rspack v2?

  • Persistent cache with proper invalidation โ€” Rspack v2 promotes persistent caching (cache.type: 'filesystem') from experimental to stable, with portable cache support (cache.portable) and read-only cache for CI (cache.readonly). This means fast rebuilds that survive process restarts and are properly invalidated when dependencies change.
  • Incremental compilation (stable) โ€” The incremental option moves from experiments to a top-level config, signaling it's production-ready. Incremental builds skip unchanged work in the dependency graph.
  • Better tree shaking โ€” CJS require() destructuring and variable property access are now tree-shaken, and Module Federation shares can be tree-shaken.
  • Unified target configuration โ€” A single target setting now propagates defaults to SWC and LightningCSS automatically, eliminating redundant per-loader configuration.
  • Stricter export validation โ€” exportsPresence defaults to 'error', catching missing or misspelled exports at build time instead of silently producing broken bundles.
  • React Server Components โ€” Built-in RSC support for frameworks.
  • Performance โ€” Dozens of Rust-level optimizations across every beta release (hash caching, regex fast paths, reduced allocations, rayon parallelism).

See the Rspack v2 breaking changes discussion for full details.

Installation

shakapacker-rspack ships shakapacker as a direct dependency and declares @rspack/core, @rspack/cli, @rspack/dev-server, and rspack-manifest-plugin as required peer dependencies. npm 7+ auto-installs those peers, so npm users can install the managed Rspack stack with one command:

npm install shakapacker-rspack -D

npm <7, Yarn Classic, pnpm, and Yarn PnP users should keep app-imported packages explicit in package.json. The default generated rspack config imports shakapacker/rspack, so list the supplemental package, shakapacker, and the required peers together:

npm install shakapacker-rspack shakapacker @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D
# or
yarn add shakapacker-rspack shakapacker @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D
# or
pnpm add shakapacker-rspack shakapacker @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D

See packages/shakapacker-rspack/README.md for the full install reference and the v10.1 supplemental packages migration guide for swapping an existing rspack install over to the supplemental package.

Manual install (self-managed versions)

If you prefer to manage @rspack/core, @rspack/cli, @rspack/dev-server, and rspack-manifest-plugin versions yourself, install them directly:

npm install @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D
# or
yarn add @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D
# or
pnpm add @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D
# or
bun add @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D

Note: These packages are already listed as optional peer dependencies in Shakapacker, so you may see warnings if they're not installed.

Configuration

To enable Rspack, update your config/shakapacker.yml:

default: &default # ... other config options
  assets_bundler: "rspack" # Change from 'webpack' to 'rspack'

Configuration Files

Rspack uses its own configuration directory to keep things organized. Create your Rspack configuration file at config/rspack/rspack.config.js:

const { generateRspackConfig } = require("shakapacker/rspack")

module.exports = generateRspackConfig()

Custom Configuration

If you need to customize your Rspack configuration:

const { generateRspackConfig } = require("shakapacker/rspack")

const rspackConfig = generateRspackConfig({
  plugins: [new SomeRspackCompatiblePlugin()],
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx"]
  }
})

module.exports = rspackConfig

Migration from Webpack Config

If you have an existing config/webpack/webpack.config.js, you can migrate it to config/rspack/rspack.config.js:

Old (webpack.config.js):

const { generateWebpackConfig } = require("shakapacker")
module.exports = generateWebpackConfig()

New (rspack.config.js):

const { generateRspackConfig } = require("shakapacker/rspack")
module.exports = generateRspackConfig()

Note: Shakapacker will show a deprecation warning if you use config/webpack/webpack.config.js with assets_bundler: 'rspack'. Please migrate to config/rspack/rspack.config.js.

Key Differences from Webpack

Built-in Loaders

Rspack has built-in loaders that are faster than their webpack counterparts:

  • JavaScript/TypeScript: Uses builtin:swc-loader instead of babel-loader
  • CSS Extraction: Uses rspack.CssExtractRspackPlugin instead of mini-css-extract-plugin
  • Asset Handling: Uses built-in asset modules instead of file-loader/url-loader

Customizing SWC on Rspack

config/swc.config.js is not read on the Rspack path. Shakapacker's built-in Rspack rule (package/rules/rspack.ts) hard-codes its builtin:swc-loader options inline. Options you put in config/swc.config.js are picked up on webpack only โ€” on Rspack they are silently ignored, with no error and no warning.

Passing module.rules into generateRspackConfig() does not replace the built-in rule. generateRspackConfig() merges your extra config with plain webpack-merge, which concatenates arrays. Your SWC rule lands alongside Shakapacker's, so both run and the built-in one still applies its own options.

To actually override the SWC options, wrap the output of generateRspackConfig() in mergeWithRules (re-exported from shakapacker/rspack) so your entry is matched into the existing rule instead of appended after it. Shakapacker's Rspack rule set has two SWC rules โ€” one for .js/.jsx/.mjs and one for .ts/.tsx โ€” so you need an entry per test regexp, or you only cover one of them:

// config/rspack/rspack.config.js
const { generateRspackConfig, mergeWithRules } = require("shakapacker/rspack")

const swcOverride = {
  use: [
    {
      loader: "builtin:swc-loader",
      options: {
        jsc: { experimental: { plugins: [["your-plugin-package", {}]] } }
      }
    }
  ]
}

module.exports = mergeWithRules({
  module: {
    rules: { test: "match", use: { loader: "match", options: "merge" } }
  }
})(generateRspackConfig(), {
  module: {
    rules: [
      { test: /\.(js|jsx|mjs)$/, ...swcOverride },
      { test: /\.(ts|tsx)$/, ...swcOverride }
    ]
  }
})

merge vs replace for the options strategy

The options strategy in the mergeWithRules spec decides whether Shakapacker's base SWC options survive, so pick it deliberately:

  • options: "merge" (used above) deep-merges your options onto Shakapacker's. The built-in jsc.parser (syntax: "ecmascript" / syntax: "typescript", jsx/tsx) and jsc.transform.react.runtime: "automatic" are preserved, and your keys are layered on top. This is what you want for adding a plugin or tweaking one option.
  • options: "replace" swaps the options object out wholesale. Shakapacker's jsc.parser and jsc.transform are gone, not overridden โ€” with the example above, Rspack would be left with no parser syntax and no automatic JSX runtime. Only use replace when you are deliberately supplying a complete SWC options object yourself.

Keep both test: "match" and loader: "match" in the spec, for different reasons. loader: "match" is what merges your entry into the existing rule instead of appending a new one. test: "match" is what pins each entry to the rule it belongs to: matching on use.loader alone leaves the rule count unchanged but applies your override to every rule that has a use chain, appending builtin:swc-loader onto the CSS and Sass chains too. And without mergeWithRules at all, the js rule is duplicated rather than updated.

Wasm SWC plugins

The common reason to reach for this is jsc.experimental.plugins, used to load a Wasm SWC plugin such as @swc/plugin-styled-components. A Wasm plugin placed only in config/swc.config.js never loads on Rspack, which is what the recipe above fixes. Wasm plugin builds must also match the swc_core version your bundler embeds โ€” Rspack 2.2 changed it, so a plugin that worked on 2.1.x can start failing after the upgrade. See Wasm plugin compatibility with Rspack for the version-matching details and the error message to look for.

Plugin Compatibility

Most webpack plugins work with Rspack, but some have Rspack-specific alternatives:

Webpack PluginRspack AlternativeStatus
mini-css-extract-pluginrspack.CssExtractRspackPluginBuilt-in
copy-webpack-pluginrspack.CopyRspackPluginBuilt-in
terser-webpack-pluginrspack.SwcJsMinimizerRspackPluginBuilt-in

Minification

Rspack uses SWC for minification by default, which is significantly faster than Terser:

optimization: {
  minimize: true,
  minimizer: [
    new rspack.SwcJsMinimizerRspackPlugin(),
    new rspack.LightningCssMinimizerRspackPlugin()
  ]
}

Shakapacker's generated Rspack config preserves the shared optimization defaults from the base config, including optimization.splitChunks.chunks = "all" and optimization.runtimeChunk = "single". In production it also preserves compression plugins and Rspack's SWC/Lightning CSS minimizers.

Module IDs and chunk IDs

Shakapacker does not set optimization.moduleIds or optimization.chunkIds itself (package/optimization/rspack.ts only configures minimize/minimizer), so Rspack's own defaults apply (moduleIds is 'deterministic' in production).

Opt-in, requires Rspack >= 2.2.0: Rspack 2.2 adds a 'compat-hashed' value for both optimization.moduleIds and optimization.chunkIds, using the shortest available prefix of a stable hash. Rspack's own benchmark โ€” measured with both options set to 'compat-hashed' together โ€” reports modest real-world savings versus 'deterministic' (~0.33% smaller minified output, ~0.87% smaller minified+gzip output). Setting only one of the two won't reproduce those numbers.

This is not a Shakapacker default, and we don't plan to make it one:

  • It requires Rspack >= 2.2.0, while Shakapacker's peer range still admits 2.0.x and 2.1.x.
  • Switching either option changes every module's or chunk's id, which invalidates all of your long-term-cached chunk hashes exactly once, on the deploy where you switch. That's a deploy-time tradeoff you should opt into deliberately, not one Shakapacker should make for you.
  • compat-hashed module ids (not chunk ids, which stay lowercase) are case-sensitive and may contain uppercase letters, per Rspack's own docs. If any output filename uses [id] as the only distinguishing placeholder for an asset or WebAssembly module filename, that's unsafe on case-insensitive filesystems (macOS and Windows, by default) โ€” prefer [contenthash], or include it alongside [id].

If you still want to opt in:

// config/rspack/rspack.config.js
const { generateRspackConfig } = require("shakapacker/rspack")

module.exports = generateRspackConfig({
  optimization: {
    moduleIds: "compat-hashed",
    chunkIds: "compat-hashed"
  }
})

Browserslist baseline targets (optional)

Requires Rspack >= 2.1.9 (Shakapacker's peer range, ^2.0.0, also admits 2.0.x and 2.1.0โ€“2.1.8, where this isn't available). From 2.1.9 on, Rspack supports targeting a Baseline browser set via target, e.g. target: 'browserslist:baseline widely available', or a date-pinned variant like target: 'browserslist:baseline widely available on 2025-05-01'. This is an opt-in override in your own Rspack config โ€” Shakapacker doesn't set target for either bundler, and the browserslist key the installer writes to package.json is unaffected either way.

Limitations

  • CoffeeScript: Not supported with Rspack
  • Some Webpack Plugins: May not be compatible; check Rspack documentation

Commands

All existing Shakapacker commands work the same way and automatically use Rspack when configured:

# Build (automatically uses rspack when assets_bundler: 'rspack')
./bin/shakapacker

# Development server (automatically uses rspack when assets_bundler: 'rspack')
./bin/shakapacker-dev-server

# Watch mode
./bin/shakapacker --watch

The same dev server configuration in shakapacker.yml applies to both webpack and rspack.

Lazy Compilation

Rspack v2 uses top-level lazyCompilation; do not rely on experiments.lazyCompilation. For Rails split dev-server topology, Shakapacker sets top-level lazyCompilation: false in the generated Rspack development config when the dev server is running. This avoids Rspack CLI dev-server auto-lazy behavior sending dynamic imports through lazy trigger URLs that Rails does not serve.

If your app has a custom, safe lazy-compilation setup, configure the top-level field explicitly in config/rspack/rspack.config.js:

const { generateRspackConfig } = require("shakapacker/rspack")

module.exports = generateRspackConfig({
  lazyCompilation: {
    imports: false,
    entries: true
  }
})

Performance Benefits

Rspack typically provides:

  • Substantially faster cold builds โ€” Rspack's own benchmark reports roughly 8x faster production builds on a 5,000-component React app (rspack.rs, benchmark sources)
  • Substantially faster development startup โ€” roughly 10โ€“15x in the same benchmark
  • Substantially faster HMR โ€” roughly 17x in the same benchmark
  • Lower memory usage in most reported cases

Actual gains depend on project size, configuration, source maps, cache state, and hardware. See Transpiler Performance Guide for measurement guidance.

Migration Checklist

  1. Install Rspack dependencies:

    npm install @rspack/core @rspack/cli @rspack/dev-server rspack-manifest-plugin -D
    
  2. Update configuration:

    # config/shakapacker.yml
    default: &default
      assets_bundler: "rspack"
    
  3. Create Rspack config:

    // config/rspack/rspack.config.js
    const { generateRspackConfig } = require("shakapacker/rspack")
    module.exports = generateRspackConfig()
    
  4. Remove CoffeeScript files (if any) - not supported by Rspack

  5. Test your application - same commands work automatically

  6. Compare the generated configs - use bin/diff-bundler-config when you are converting custom webpack configuration or need to prove the rspack config kept the same entrypoints, loaders, output paths, and plugin intent.

Troubleshooting

Configuration Issues

If you encounter configuration issues:

  1. Check that all plugins are Rspack-compatible
  2. Verify custom loaders work with Rspack
  3. Review the Rspack migration guide

Performance Issues

If builds are unexpectedly slow:

  1. Ensure you're using built-in Rspack loaders
  2. Check for webpack-specific plugins that should be replaced
  3. Review your asset optimization settings

Further Reading