Introduction to SWC
SWC (Speedy Web Compiler) is a super-fast TypeScript/JavaScript compiler written in Rust. It can be used for both compilation and bundling, and it is designed to be a drop-in replacement for tools like Babel, with performance that often exceeds it by an order of magnitude or more. SWC is the engine behind many modern build tools, including Next.js, Parcel, Deno, and Turbopack.
Whether you are transpiling TypeScript, transforming JSX, minifying production bundles, or building your own custom codemod, SWC offers a robust and extensible platform. This tutorial walks you through the entire learning path, from your first installation to writing custom plugins and integrating SWC into production-grade tooling.
What SWC Is and Why It Matters
The Problem SWC Solves
As JavaScript applications grow, transpilation becomes a bottleneck. Babel, while incredibly flexible and ecosystem-rich, is written in JavaScript and processes files single-threaded for the most part. On large codebases, a full Babel transpilation pass can take minutes. SWC solves this by:
- Being written in Rust, which compiles to native machine code and avoids the overhead of a JavaScript runtime.
- Using parallel processing by default, distributing work across all available CPU cores.
- Providing a unified toolchain for parsing, transformation, code generation, and minification.
- Offering a plugin system that is compatible with Babel's mental model but with native performance.
SWC vs Babel vs esbuild
SWC sits between Babel and esbuild on the flexibility-performance spectrum. Babel is the most flexible but slowest. esbuild is the fastest but has a more limited plugin API. SWC aims to provide both high performance and a rich plugin ecosystem, making it suitable for projects that need custom transformations without sacrificing speed.
Getting Started: Installing SWC
The easiest way to start using SWC is through its official CLI. You can install it globally or locally in your project.
npm install --save-dev @swc/cli @swc/core
Once installed, you can transpile a file directly from the command line:
npx swc src/index.ts -o dist/index.js
You can also transpile an entire directory:
npx swc src -d dist
This will read every supported file in src and write the compiled output to dist. By default, SWC will strip TypeScript types and transform JSX, but you will want a configuration file for anything more advanced.
Configuring SWC with .swcrc
SWC reads configuration from a .swcrc file in your project root. This JSON file controls every aspect of compilation. Here is a minimal configuration that handles TypeScript and modern JavaScript syntax:
{
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": true
},
"target": "es2020",
"loose": false,
"minify": {
"compress": false,
"mangle": false
}
},
"module": {
"type": "commonjs"
}
}
The configuration is split into three main sections: jsc for JavaScript/TypeScript compiler options, module for module system configuration, and top-level options for minification and source maps.
Understanding the jsc Section
The jsc section is where most of your configuration lives. The parser subsection tells SWC how to parse your source files. You can switch between typescript and ecmascript syntax modes. The target field determines which ECMAScript version to compile down to. Setting loose to true relaxes some transformations for smaller output at the cost of strict spec compliance.
Module Configuration
SWC can transform between module systems. This is useful when your source code uses ES modules but your target environment requires CommonJS:
{
"module": {
"type": "commonjs",
"strict": true,
"strictMode": true,
"lazy": false
}
}
Supported module types include commonjs, es6, amd, umd, and systemjs. The lazy option defers requiring of CommonJS modules until they are actually used, which can improve startup time in Node.js applications.
Using SWC Programmatically
Beyond the CLI, SWC exposes a Node.js API through @swc/core. This is essential when you want to embed SWC into your own build scripts or tools.
Basic Compilation
const swc = require("@swc/core");
const code = `
interface User {
name: string;
age: number;
}
const greet = (user: User): string => {
return \`Hello, \${user.name}!\`;
};
`;
const output = swc.transformSync(code, {
filename: "example.ts",
jsc: {
parser: {
syntax: "typescript",
},
target: "es2020",
},
});
console.log(output.code);
The transformSync function is useful for scripts where blocking is acceptable. For web servers or build pipelines, prefer the asynchronous transform method:
const swc = require("@swc/core");
async function compile(code) {
const output = await swc.transform(code, {
filename: "example.ts",
jsc: {
parser: { syntax: "typescript" },
target: "es2020",
},
});
return output.code;
}
compile("const x: number = 42;").then(console.log);
Parsing and Code Generation
For advanced use cases, you can separate parsing from transformation and code generation. This gives you access to the AST directly:
const swc = require("@swc/core");
const code = "const add = (a, b) => a + b;";
// Parse the code into an AST
const ast = swc.parseSync(code, {
syntax: "ecmascript",
jsx: false,
});
// Transform the AST
const transformed = swc.transformSync(code, {
jsc: {
parser: { syntax: "ecmascript" },
target: "es5",
},
});
console.log(transformed.code);
// Output: var add = function(a, b) { return a + b; };
Working with the SWC AST
To become an expert with SWC, you need to understand its Abstract Syntax Tree (AST). The AST is a tree representation of your source code that SWC uses internally for all transformations. Every node in the tree corresponds to a syntactic construct: a function declaration, a variable, a binary expression, and so on.
Inspecting the AST
You can inspect the AST by parsing code and logging the result. Here is an example that parses a simple function and prints its structure:
const swc = require("@swc/core");
const code = `
function double(x) {
return x * 2;
}
`;
const ast = swc.parseSync(code, {
syntax: "ecmascript",
});
console.log(JSON.stringify(ast, null, 2));
The output will show a Module node containing a body array. Each element in the body is a statement. For the function above, you will see a FunctionDeclaration node with properties like identifier, params, and body.
AST Node Types You Should Know
Module: The root node representing an entire file.FunctionDeclaration: A named function declaration.ArrowFunctionExpression: An arrow function.VariableDeclaration: Avar,let, orconstdeclaration.CallExpression: A function call likefoo().MemberExpression: Property access likeobj.property.BinaryExpression: Operations likea + b.JsxElementandJsxFragment: JSX-specific nodes.
Writing Custom SWC Plugins
SWC plugins are the key to extending its functionality. There are two types of plugins: JavaScript plugins (written in JavaScript/TypeScript using the SWC Node API) and native plugins (written in Rust using the swc_plugin crate). JavaScript plugins are easier to write and are suitable for most use cases.
JavaScript Plugins
A JavaScript plugin is a function that receives a configuration object and returns a visitor object. The visitor pattern lets you hook into specific AST node types and transform them. Here is a complete plugin that converts all string literals to uppercase:
const swc = require("@swc/core");
const { Visitor } = require("@swc/core/visitor");
class UppercaseStrings extends Visitor {
visitStringLiteral(node) {
node.value = node.value.toUpperCase();
return node;
}
}
const plugin = (options) => {
return (program) => {
const visitor = new UppercaseStrings();
return visitor.visitProgram(program);
};
};
module.exports = plugin;
To use this plugin, reference it in your .swcrc:
{
"jsc": {
"parser": {
"syntax": "ecmascript"
},
"target": "es2020",
"experimental": {
"plugins": [
["./plugins/uppercase-strings.js", {}]
]
}
}
}
A Practical Plugin: Console Statement Removal
A common use case for custom plugins is removing console statements in production builds. Here is how you would implement that:
const { Visitor } = require("@swc/core/visitor");
class StripConsole extends Visitor {
visitCallExpression(node) {
node.callee = this.visitExpression(node.callee);
node.arguments = node.arguments.map((arg) => this.visitExpression(arg));
// Check if this is a console.* call
if (
node.callee.type === "MemberExpression" &&
node.callee.object.type === "Identifier" &&
node.callee.object.value === "console"
) {
// Replace with undefined
return {
type: "Identifier",
span: node.span,
value: "undefined",
optional: false,
};
}
return node;
}
}
const plugin = (options) => {
return (program) => {
const visitor = new StripConsole();
return visitor.visitProgram(program);
};
};
module.exports = plugin;
This plugin walks every call expression, checks whether the callee is a member of console, and replaces the entire call with undefined. In a real-world scenario, you might want to remove the entire statement rather than replacing it with undefined, but this demonstrates the core technique.
Minification with SWC
SWC includes a built-in minifier that can replace tools like Terser for many projects. Minification is controlled through the minify option in your configuration:
{
"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": true
},
"target": "es2020",
"minify": {
"compress": {
"arguments": true,
"booleans": true,
"collapse_vars": true,
"comparisons": true,
"computed_props": true,
"conditionals": true,
"dead_code": true,
"drop_console": false,
"drop_debugger": true,
"ecma": 2020,
"evaluate": true,
"expression": true,
"hoist_funs": false,
"hoist_props": true,
"hoist_vars": false,
"if_return": true,
"join_vars": true,
"keep_classnames": false,
"keep_fnames": false,
"loops": true,
"negate_iife": true,
"properties": true,
"reduce_funcs": true,
"reduce_vars": true,
"sequences": true,
"side_effects": true,
"switches": true,
"typeofs": true,
"unused": true
},
"mangle": {
"toplevel": false,
"keep_classnames": false,
"keep_fnames": false,
"keep_private_props": false,
"ie8": false,
"safari10": false
}
}
},
"minify": true
}
You can also invoke minification programmatically:
const swc = require("@swc/core");
const code = `
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
return total;
}
`;
const result = swc.minifySync(code, {
compress: true,
mangle: true,
ecma: 2020,
});
console.log(result.code);
// Output: function calculateTotal(n){let t=0;for(let l=0;l<n.length;l++)t+=n[l].price*n[l].quantity;return t}
Integrating SWC with Bundlers
SWC with Webpack
To use SWC with Webpack, install swc-loader and add it to your webpack configuration:
npm install --save-dev swc-loader
// webpack.config.js
module.exports = {
entry: "./src/index.ts",
output: {
filename: "bundle.js",
path: __dirname + "/dist",
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: "swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
tsx: true,
decorators: true,
},
transform: {
react: {
pragma: "React.createElement",
pragmaFrag: "React.Fragment",
runtime: "automatic",
},
},
target: "es2020",
},
},
},
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
};
SWC with Rollup
For Rollup, use @rollup/plugin-swc:
npm install --save-dev @rollup/plugin-swc
// rollup.config.js
import { swc } from "@rollup/plugin-swc";
export default {
input: "src/index.ts",
output: {
file: "dist/bundle.js",
format: "esm",
},
plugins: [
swc({
jsc: {
parser: {
syntax: "typescript",
tsx: true,
},
target: "es2020",
transform: {
react: {
runtime: "automatic",
},
},
},
}),
],
};
SWC for React and JSX
SWC is an excellent choice for React projects because it handles JSX transformation natively and quickly. The React transform configuration lives under jsc.transform.react:
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true
},
"transform": {
"react": {
"runtime": "automatic",
"development": false,
"pragma": "React.createElement",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"refresh": true
}
},
"target": "es2020"
}
}
Setting runtime to "automatic" uses the new JSX transform introduced in React 17+, which means you no longer need to import React at the top of every file. The refresh option enables React Fast Refresh, which is essential for development workflows.
Source Maps and Debugging
Source maps are critical for debugging transpiled code. SWC generates source maps when you enable the sourceMaps option:
{
"sourceMaps": true,
"jsc": {
"parser": {
"syntax": "typescript"
},
"target": "es2020"
}
}
Programmatically, source maps are returned as part of the output object:
const swc = require("@swc/core");
const result = await swc.transform("const x: number = 42;", {
filename: "example.ts",
sourceMaps: true,
jsc: {
parser: { syntax: "typescript" },
target: "es2020",
},
});
console.log(result.code);
console.log(result.map);
// The map is a JSON string you can write to a .js.map file
Advanced Topic: Native Rust Plugins
For maximum performance, SWC supports native plugins written in Rust. These plugins are compiled to WebAssembly and loaded by the SWC core. While JavaScript plugins are easier to write, native plugins can be significantly faster because they run in the same process as the SWC core without crossing the JavaScript-Rust boundary.
Here is the skeleton of a native SWC plugin in Rust:
// Cargo.toml
[package]
name = "swc-plugin-example"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
swc_core = { version = "0.90", features = ["ecma_plugin_transform"] }
[profile.release]
lto = true
opt-level = 3
// src/lib.rs
use swc_core::ecma::ast::Program;
use swc_core::ecma::visit::{as_folder, FoldWith, VisitMut};
use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata};
struct TransformVisitor;
impl VisitMut for TransformVisitor {
// Implement visit_mut_* methods for each node type you want to transform
}
#[plugin_transform]
pub fn process_transform(program: Program, _metadata: TransformPluginProgramMetadata) -> Program {
program.fold_with(&mut as_folder(TransformVisitor))
}
After writing your plugin, compile it with cargo build --release --target wasm32-wasi and reference the resulting .wasm file in your .swcrc:
{
"jsc": {
"experimental": {
"plugins": [
["./target/wasm32-wasi/release/swc_plugin_example.wasm", {}]
]
}
}
}
Best Practices
1. Pin Your SWC Versions
SWC is under active development and breaking changes can occur between minor versions. Always pin your @swc/core version in package.json and update deliberately after testing.
2. Use Environment-Specific Configurations
Development and production builds have different needs. Use separate configuration files or environment variables to switch between them:
// swc.config.js
module.exports = (env) => {
const isProduction = env.NODE_ENV === "production";
return {
jsc: {
parser: {
syntax: "typescript",
tsx: true,
},
target: "es2020",
transform: {
react: {
runtime: "automatic",
development: !isProduction,
refresh: !isProduction,
},
},
minify: isProduction
? {
compress: {
drop_console: true,
drop_debugger: true,
},
mangle: true,
}
: false,
},
sourceMaps: !isProduction,
minify: isProduction,
};
};
3. Cache Transpilation Results
In CI/CD pipelines and large monorepos, caching transpiled output can save significant time. Use tools like swc-loader with Webpack's cache or integrate SWC with a persistent cache layer.
4. Benchmark Before Committing
Before adopting SWC as a replacement for Babel, benchmark your existing build and compare. While SWC is almost always faster, the magnitude of improvement depends on your codebase size, the number of plugins, and your hardware.
5. Keep Plugin Logic Simple
When writing custom plugins, keep the transformation logic as simple and focused as possible. Complex plugins are harder to maintain and more likely to introduce subtle bugs. If a transformation requires multiple passes, consider splitting it into separate plugins.
6. Leverage SWC's Built-in Transforms
Before writing a custom plugin, check whether SWC already supports the transformation you need. SWC includes built-in support for decorators, class properties, optional chaining, nullish coalescing, and many other modern JavaScript features. Using built-in transforms is always faster than custom plugins.
7. Test Your Transformations
Always write tests for your SWC configurations and custom plugins. Use snapshot testing to catch unexpected changes in output:
const swc = require("@swc/core");
describe("SWC configuration", () => {
test("transpiles TypeScript correctly", async () => {
const input = "const x: number = 42;";
const output = await swc.transform(input, {
filename: "test.ts",
jsc: {
parser: { syntax: "typescript" },
target: "es2020",
},
});
expect(output.code).toContain("var x");
});
test("removes console statements in production", async () => {
const input = "console.log('debug');";
const output = await swc.transform(input, {
filename: "test.js",
jsc: {
parser: { syntax: "ecmascript" },
target: "es2020",
minify: {
compress: { drop_console: true },
},
},
minify: true,
});
expect(output.code).not.toContain("console.log");
});
});
Common Pitfalls and Troubleshooting
Decorator Metadata Not Working
If you are using TypeScript decorators that rely on emitDecoratorMetadata (common with NestJS and TypeORM), make sure both decorators and decoratorMetadata are enabled in your parser configuration:
{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true,
"decoratorMetadata": true
}
}
}
JSX Not Being Transformed
If your JSX is not being transformed, check that tsx is set to true in the parser configuration and that the file extension matches your test pattern in your bundler configuration. Also verify that jsc.transform.react is configured.
Plugin Not Loading
If your custom plugin is not being applied, verify the path in .swcrc is correct relative to the file being compiled, not relative to the configuration file. Also ensure the plugin exports a function that returns a transformer function.
Conclusion
SWC has established itself as a cornerstone of modern JavaScript and TypeScript tooling, offering unparalleled compilation speed without sacrificing flexibility. By progressing from basic CLI usage through programmatic APIs, AST manipulation, custom plugin development, and native Rust plugins, you now have the full spectrum of SWC's capabilities at your disposal. The key to mastering SWC is understanding its layered architecture: the parser produces an AST, visitors traverse and transform that AST, and the code generator produces the final output. Whether you are optimizing a large-scale monorepo, building developer tooling, or creating codemods for your team, SWC provides the performance and extensibility to handle it all. Start with the basics, write small plugins to understand the AST, and gradually move toward more complex transformations and native plugins as your needs grow. With the best practices and patterns covered in this tutorial, you are well-equipped to leverage SWC effectively in any project.