Using SWC Loader
August 29, 2026 · View on GitHub
SWC is the recommended JavaScript transpiler in Shakapacker v10 (this default was introduced in v9), and is set as the default in new installations. If you face any issues, please report them at Shakapacker Issues.
About SWC
SWC (Speedy Web compiler) is a Rust-based compilation and bundler tool that can be used for Javascript and Typescript files. SWC's own benchmark reports being 20x faster than Babel on a single thread and 70x faster on four cores; end-to-end Shakapacker build wins are typically smaller than that pure-transpiler number but still substantial.
It supports all ECMAScript features and it's designed to be a drop-in replacement for Babel and its plugins. Out of the box, it supports TS, JSX syntax, React fast refresh, and much more.
For comparison between SWC and Babel, see the docs at https://swc.rs/docs/migrating-from-babel.
Note: SWC is also natively built into RSpack bundler, providing even faster compilation speeds. When using RSpack (
assets_bundler: 'rspack'), SWC is the default ifjavascript_transpileris not explicitly set.
Using SWC in your Shakapacker project
For new installations of Shakapacker v9+, SWC is automatically configured in the installation template.
Note: While the installation template sets SWC as the default, webpack's runtime fallback (when no explicit config exists) remains Babel for backward compatibility. Rspack always defaults to SWC.
If you're upgrading from v8 or earlier and want to switch from Babel to SWC:
- Make sure you've installed
@swc/coreandswc-loaderpackages.
npm install @swc/core swc-loader
- Confirm
javascript_transpileris set toswcin yourconfig/shakapacker.yml:
default: &default
source_path: app/javascript
source_entry_path: /
public_root_path: public
public_output_path: packs
cache_path: tmp/shakapacker
webpack_compile_output: true
# Additional paths webpack should look up modules
# ['app/assets', 'engine/foo/app/assets']
additional_paths: []
# Reload manifest.json on all requests so we reload latest compiled packs
cache_manifest: false
# Select JavaScript transpiler to use
# Available options: 'swc' (default, ~20x faster than Babel per swc.rs), 'babel', or 'esbuild'
# Note: When using rspack, swc is used automatically regardless of this setting
javascript_transpiler: "swc"
Usage
React
React is supported out of the box, provided you use .jsx or .tsx file extension. Shakapacker config will correctly recognize those and tell SWC to parse the JSX syntax correctly. If you wish to customize the transform options to match any existing @babel/preset-react settings, you can do that through customizing loader options as described below. You can see available options at https://swc.rs/docs/configuration/compilation#jsctransformreact.
Typescript
Typescript is supported out of the box, but certain features like decorators need to be enabled through the custom config. You can see available customizations options at https://swc.rs/docs/configuration/compilation, which you can apply through customizing loader options as described below.
Please note that SWC is not using the settings from .tsconfig file. Any non-default settings you might have there will need to be applied to the custom loader config.
Customizing loader options
You can see the default loader options at swc/index.js.
If you wish to customize the loader defaults further, for example, if you want to enable support for decorators or React fast refresh, you need to create a swc.config.js file in your app config folder.
This file should have a single default export which is an object with an options key. Your customizations will be merged with default loader options. You can use this to override or add additional configurations.
Inside the options key, you can use any options available to the SWC compiler. For the options reference, please refer to official SWC docs.
See some examples below of potential config/swc.config.js.
Example: Enabling top level await and decorators
const customConfig = {
options: {
jsc: {
parser: {
topLevelAwait: true,
decorators: true
}
}
}
}
module.exports = customConfig
Example: Matching existing @babel/present-env config
const { env } = require("shakapacker")
const customConfig = {
options: {
jsc: {
transform: {
react: {
development: env.isDevelopment,
useBuiltins: true
}
}
}
}
}
module.exports = customConfig
Example: Enabling React Fast Refresh
:warning: Shakapacker's default development config automatically adds @pmmmwh/react-refresh-webpack-plugin when HMR is enabled and the package is installed. Use the setting below when you need to customize the SWC React transform itself; it replaces the equivalent react-refresh/babel Babel plugin behavior.
const { env } = require("shakapacker")
const customConfig = {
options: {
jsc: {
transform: {
react: {
refresh: env.isDevelopment && env.runningWebpackDevServer
}
}
}
}
}
module.exports = customConfig
Example: Adding browserslist config
const customConfig = {
options: {
env: {
targets: "> 0.25%, not dead"
}
}
}
module.exports = customConfig
Using SWC with Stimulus
⚠️ Important: If you're using Stimulus, you need to configure SWC to preserve class names.
Required Configuration
SWC mangles (minifies) class names by default for optimization. Since Stimulus relies on class names to discover and instantiate controllers, you must preserve class names. On webpack, do this in your config/swc.config.js:
// config/swc.config.js
const { env } = require("shakapacker")
module.exports = {
options: {
jsc: {
// CRITICAL for Stimulus: Prevents SWC from mangling class names
keepClassNames: true,
transform: {
react: {
runtime: "automatic",
refresh: env.isDevelopment && env.runningWebpackDevServer
}
}
}
}
}
On Rspack, config/swc.config.js is not read — see Customizing SWC on Rspack to apply keepClassNames: true there instead.
Note: Starting with Shakapacker v9.1.0, the default swc.config.js created by rake shakapacker:migrate_to_swc includes keepClassNames: true automatically.
Why This Matters
Without keepClassNames: true, your Stimulus controllers will:
- Load without errors in the browser console
- Fail silently at runtime
- Not respond to events
- Not update the DOM as expected
This makes debugging very difficult since there are no visible JavaScript errors.
Symptoms of Missing Configuration
If your Stimulus controllers aren't working after migrating to SWC, you'll typically see test failures like:
Failure/Error: expect(page).to have_text("Author: can't be blank")
expected to be truthy, got false
Failure/Error: expect(page).to have_css("h2", text: comment.author)
expected to be truthy, got false
Your controllers appear to load but don't function correctly:
- Form submissions don't work
- Validation error messages don't appear
- Dynamic content doesn't get added to the page
- No JavaScript errors appear in the console
Common Configuration Error
❌ Error: `env` and `jsc.target` cannot be used together
If you see this error:
ERROR in ./client/app/packs/stimulus-bundle.js
Module build failed (from ./node_modules/swc-loader/src/index.js):
Error:
Caused by:
`env` and `jsc.target` cannot be used together
Solution: Do NOT add jsc.target to your configuration. Shakapacker already sets env for browser targeting. Use env OR jsc.target, never both.
❌ Incorrect:
jsc: {
target: 'es2015', // Don't add this!
keepClassNames: true,
}
✅ Correct:
jsc: {
keepClassNames: true, // No target specified
}
Troubleshooting Checklist
If your Stimulus controllers aren't working after migrating to SWC:
- ✅ Verify
keepClassNames: trueis set inconfig/swc.config.js(webpack) or in yourbuiltin:swc-loaderoverride (Rspack — see Customizing SWC on Rspack) - ✅ Ensure your controllers have explicit class names (not anonymous classes)
- ✅ Test with
console.log()in your controller'sconnect()method to verify it's being instantiated - ✅ Check that you haven't added
jsc.target(which conflicts with Shakapacker'senvsetting) - ✅ Rebuild your assets:
bundle exec rake shakapacker:clobber && bundle exec rake shakapacker:compile
Wasm plugin compatibility with Rspack
If you use jsc.experimental.plugins to load a Wasm SWC plugin (for example @swc/plugin-styled-components or swc-plugin-coverage-instrument), the plugin build needs to be compatible with the swc_core version your bundler uses. Per SWC's own docs, "the Wasm plugins are not backwards compatible." SWC has improved cross-version compatibility for newer plugin builds since introducing a more stable plugin ABI, but that compatibility isn't unlimited, and exactly how far it extends for any given plugin/host pairing isn't something we can state as a general rule here — the reliable way to check is plugins.swc.rs: select your bundler version and it lists which plugin builds are known to work. This applies to Rspack's builtin:swc-loader (Rspack's own SWC integration) just as much as to swc-loader on webpack.
Where you configure this differs by bundler. On webpack, jsc.experimental.plugins goes in config/swc.config.js like any other option in Customizing loader options above — Shakapacker's webpack SWC rule reads that file and merges it in. On Rspack, it does not: Shakapacker's built-in Rspack rule hard-codes its builtin:swc-loader options inline and never reads config/swc.config.js. A Wasm plugin placed only in config/swc.config.js is silently ignored on the Rspack path — no error, it just never loads.
If you're on Rspack and need jsc.experimental.plugins, passing a module.rules override straight to generateRspackConfig() won't work either — it merges via plain concatenation, so your rule gets added alongside Shakapacker's built-in one instead of replacing it, and the plugin may silently never run. Use mergeWithRules (re-exported from shakapacker/rspack) to merge into the existing rules instead. Shakapacker's built-in Rspack rule set has two separate rules — one for .js/.jsx/.mjs, one for .ts/.tsx — so the override needs an entry matching each test regexp you need, or your plugin only applies to whichever one you covered:
// config/rspack/rspack.config.js
const { generateRspackConfig, mergeWithRules } = require("shakapacker/rspack")
const pluginOptions = {
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)$/, ...pluginOptions },
{ test: /\.(ts|tsx)$/, ...pluginOptions }
]
}
})
That's also where the fix below applies.
Rspack 2.2 upgraded swc_core from 76 to 77. If you're on assets_bundler: 'rspack' and upgrade to Rspack 2.2, a Wasm plugin that worked fine on Rspack 2.1.x can start failing with:
The version of the SWC Wasm plugin you're using might not be compatible with 'builtin:swc-loader'
Fix:
- On Rspack: get a plugin build that plugins.swc.rs lists as compatible with your installed Rspack version. Rspack 2.2 embeds
swc_core77, but a later Rspack release will embed a different version again — check your actual installed release rather than assuming 77 stays correct going forward. - On webpack: there's no bundler-embedded core to match against —
swc-loaderruns against your application's own independently installed@swc/core(whatever version is in yourpackage.json), entirely independent of your webpack version. Match the plugin build to that installed@swc/coreversion instead. - Alternatively, on Rspack you can pin your Rspack packages (
@rspack/core,@rspack/cli, etc.) to< 2.2.0. This isn't a guaranteed fix for every plugin — whether it works depends on whether your plugin build is one that plugins.swc.rs lists as compatible with the pre-2.2swc_core.
See Rspack's SWC plugin version mismatch error reference for more detail, and the Troubleshooting guide for the same guidance in context.
Known limitations
browserslistconfig at the moment is not being picked up automatically. Related SWC issue. You can add your browserlist config through customizing loader options as outlined above.- On webpack,
.swcrcis read, but it completely overrides Shakapacker's default SWC settings instead of merging with them, which can cause build failures — useconfig/swc.config.jsinstead (see Customizing loader options above). On Rspack,.swcrcis not read at all — Rspack'sbuiltin:swc-loaderignores it entirely, so any settings there are silently inert. See Customizing SWC on Rspack to override SWC options on Rspack instead.