React 129 ⚛️ AST Transformations and Custom Babel/SWC Plugins for React
Every React build is a sequence of AST transformations. JSX becomes React.createElement or jsx() calls, TypeScript types are stripped, and the React Compiler inserts memoization. The tools that perform these transformations—Babel and SWC—expose plugin APIs that let developers add their own passes to the pipeline. A custom plugin can inject instrumentation, rewrite imports, enforce architectural rules, or generate code from a domain-specific notation, and the choice between Babel and SWC determines the language the plugin is written in and the performance it delivers.
This chapter covers AST transformations as they apply to React tooling. It begins with the Babel plugin API—the visitor pattern, the pre and post hooks, and the path object that represents a node’s position in the tree. It then examines the React Compiler as the most consequential Babel plugin in the ecosystem, its multi-phase pipeline, and the opt-in configuration that controls which components it transforms. The chapter then addresses SWC, its Rust-based plugin system, and the performance trade-off that makes it the default in Next.js and Vite. The final sections cover the tooling for inspecting ASTs and the decision criteria that separate the two.
Key point: A Babel or SWC plugin is a function that receives the AST and mutates it. The visitor pattern determines which nodes the function sees. The plugin does not parse the source or generate the output; the tool does that. The plugin’s only job is to transform the tree.
Why AST transformations are a React concern
The JSX transform. JSX is not JavaScript. The <div /> syntax must be converted to a function call before the browser can execute it. The @babel/preset-react and SWC’s jsc.transform.react configuration perform this conversion. The choice between the classic runtime (React.createElement) and the automatic runtime (jsx() from react/jsx-runtime) is a configuration decision that determines whether the file needs a React import . A custom plugin that transforms JSX must account for the runtime the project uses.
The React Compiler. The React Compiler is a Babel plugin that analyzes components and inserts memoization automatically. It is the most complex Babel plugin in the React ecosystem, and its configuration—opt-in versus opt-out, file-based source selection, and compilation modes—determines whether it transforms a given component . Understanding its pipeline is understanding the current state of React build tooling.
The performance difference. Babel is written in JavaScript and runs on Node.js. SWC is written in Rust and runs natively. On a medium-sized React project, the difference is roughly 14–18x in compilation speed . The trade-off is the plugin ecosystem: Babel has thousands of plugins, and SWC has fewer, though the core transformations are covered.
The plugin language. A Babel plugin is a JavaScript function. A SWC plugin is a Rust crate compiled to a dynamic library. The language determines the developer’s skill requirement and the plugin’s performance. A team that needs a custom transformation must decide whether to write it in JavaScript (Babel, slower but more accessible) or Rust (SWC, faster but requires Rust knowledge) .
a. The Babel plugin API: visitors, paths, and hooks
A Babel plugin is a function that returns an object with a visitor property. The visitor is an object whose keys are AST node types and whose values are functions that run when the traversal reaches a node of that type . The function receives a path object, which represents the node’s position in the tree and provides methods for inspection and mutation.
// A Babel plugin that renames the identifier "foo" to "bar".
module.exports = function myPlugin() {
return {
visitor: {
Identifier(path) {
// path.node is the AST node.
// path.parent is the parent node.
if (path.node.name === 'foo') {
path.node.name = 'bar';
}
},
},
};
};
The path object is the primary interface. It has node (the AST node), parent (the parent node), and methods like replaceWith, insertBefore, insertAfter, remove, and skip. The path.scope object tracks the bindings in the current scope, which is how a plugin determines whether an identifier refers to a local variable or a global . The skip() method stops the traversal from descending into the current node’s children, which is useful when a transformation has already handled them.
The plugin can also define pre and post hooks. The pre hook runs before the traversal, and the post hook runs after. They receive the file object, which contains the AST, the source code, and metadata. The pre hook is the place to initialize state that the visitor functions need across nodes .
module.exports = function myPlugin() {
let transformedCount = 0;
return {
pre(file) {
// Runs before traversal.
this.transformedCount = 0;
},
visitor: {
Identifier(path) {
if (path.node.name === 'foo') {
path.node.name = 'bar';
this.transformedCount++;
}
},
},
post(file) {
// Runs after traversal.
console.log(`Transformed ${this.transformedCount} identifiers`);
},
};
};
The @babel/types package provides builders for creating AST nodes. A plugin that inserts a new expression uses t.callExpression, t.identifier, and t.stringLiteral to construct the node, rather than string manipulation. Building nodes from the type library preserves source locations and ensures the output is valid .
b. The React Compiler as a Babel plugin
The React Compiler is configured in babel.config.js as babel-plugin-react-compiler. It analyzes components and hooks, infers mutation and aliasing, and generates memoized code. The configuration options determine which components it processes .
The sources option controls the opt-in/opt-out policy. It is a function that receives a filename and returns one of 'all', 'none', 'opt-in', or 'opt-out'. The 'opt-in' mode compiles only components with a 'use memo' directive; the 'opt-out' mode compiles everything except components with 'use no memo' . The file-based logic can route different directories to different policies.
// babel.config.js — React Compiler opt-in for new features.
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
sources: (filename) => {
if (filename.includes('__tests__')) return 'none';
if (filename.includes('src/features/new')) return 'opt-in';
if (filename.includes('src/legacy')) return 'opt-out';
return 'all';
},
}],
],
};
The compiler’s internal pipeline has seven phases and over 55 distinct passes. It lowers the Babel AST to a High-level Intermediate Representation (HIR) with a control-flow graph, converts to Static Single Assignment (SSA) form, performs constant propagation and dead code elimination, infers mutation and aliasing, groups co-mutating variables into reactive scopes, converts the HIR back to a ReactiveFunction tree, and generates the final Babel AST with memoization . The phase structure is the reason the compiler can make optimization decisions that a simple AST visitor cannot.
The compilation modes are 'all_features' (the default) and 'no_inferred_memo' . The environment options include validateHooksUsage, which checks that hooks follow the rules of hooks, and enablePreserveExistingMemoizationGuarantees, which preserves existing useMemo and useCallback calls rather than replacing them. The enableReanimatedCheck option adds compatibility checking for React Native Reanimated .
c. SWC: the Rust alternative and its plugin model
SWC is a Rust-based compiler that performs the same transformations as Babel—JSX, TypeScript stripping, ES module conversion—but at native speed. It is the default compiler in Next.js 12+ and can be used in Vite via vite-plugin-swc-react . The configuration lives in .swcrc or in the bundler’s config, and the React-specific options control the JSX runtime, development mode, and Fast Refresh .
// .swcrc — React configuration
{
"jsc": {
"parser": { "syntax": "typescript", "tsx": true },
"transform": {
"react": {
"runtime": "automatic",
"development": false,
"refresh": true
}
}
}
}
The runtime: "automatic" setting uses React 17+’s JSX transform, which does not require a React import. The runtime: "classic" setting uses the older transform, which does. The importSource option changes the JSX pragma source, which is useful for libraries like Emotion that require a custom import. The refresh: true option enables React Fast Refresh for hot module replacement .
SWC plugins are written in Rust and compiled to dynamic libraries. The plugin’s entry point must match the SWC ABI, and the .swcrc plugins field must point to the absolute path of the compiled .so, .dylib, or .dll file. The plugin implements the VisitMut trait, which provides mutable access to the AST nodes . The Visit trait is read-only and is used for analysis or code generation rather than transformation.
The performance advantage is measurable. On a medium-sized React TypeScript project with roughly 500 components, a cold build with Babel is around 45 seconds, while SWC completes in 3.2 seconds—a 14x difference. Hot reload for a single file is 800ms with Babel and 45ms with SWC. The production build is 2m15s versus 8.5s . The difference is the language and the architecture: Rust’s native execution and SWC’s parallel processing avoid the Node.js overhead that Babel incurs.
The trade-offs are the plugin ecosystem and TypeScript type checking. SWC strips TypeScript types but does not check them; tsc --noEmit is still required. The plugin ecosystem is smaller, and some Babel plugins have no SWC equivalent. For projects with heavy plugin dependencies, the migration to SWC is not a drop-in replacement .
Complete Example Session
// ============================================
// PART 1: THE BABEL PLUGIN STRUCTURE
// ============================================
// A plugin is a function returning a visitor object.
module.exports = function myPlugin() {
return {
visitor: {
Identifier(path) {
// Runs for every Identifier node.
},
},
};
};
// ============================================
// PART 2: THE PATH OBJECT
// ============================================
// path.node, path.parent, path.scope, path.replaceWith.
module.exports = function renamePlugin() {
return {
visitor: {
Identifier(path) {
if (path.node.name === 'oldName') {
path.node.name = 'newName';
}
},
},
};
};
// ============================================
// PART 3: THE PRE AND POST HOOKS
// ============================================
// pre runs before traversal, post after.
module.exports = function countingPlugin() {
return {
pre() { this.count = 0; },
visitor: {
CallExpression(path) {
this.count++;
},
},
post() { console.log(`Found ${this.count} calls`); },
};
};
// ============================================
// PART 4: THE REACT COMPILER CONFIGURATION
// ============================================
// Opt-in for new features, opt-out for legacy.
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
sources: (filename) => {
if (filename.includes('legacy')) return 'opt-out';
if (filename.includes('features')) return 'opt-in';
return 'all';
},
}],
],
};
// ============================================
// PART 5: THE USE MEMO DIRECTIVE
// ============================================
// Opt-in mode requires the directive.
function MyComponent(props) {
'use memo';
// The compiler will transform this component.
return <div>{props.value}</div>;
}
// ============================================
// PART 6: THE SWC CONFIGURATION
// ============================================
// The .swcrc controls JSX and React refresh.
// {
// "jsc": {
// "transform": {
// "react": {
// "runtime": "automatic",
// "refresh": true
// }
// }
// }
// }
// ============================================
// PART 7: THE SWC PLUGIN (RUST)
// ============================================
// A VisitMut implementation for transformation.
// use swc_core::ecma::{ast::*, visit::{VisitMut, VisitMutWith}};
//
// pub struct MyPlugin;
//
// impl VisitMut for MyPlugin {
// fn visit_mut_identifier(&mut self, ident: &mut Ident) {
// if ident.sym == *"oldName" {
// ident.sym = "newName".into();
// }
// }
// }
// ============================================
// PART 8: THE AST INSPECTION
// ============================================
// Use AST Explorer or astexplore-mcp to see the tree.
// astexplore-mcp: parse code, get AST as JSON.
// npx astexplore-mcp parse "const x = 1" --parser babel
// ============================================
// PART 9: THE PERFORMANCE COMPARISON
// ============================================
// Babel: 45s initial, 800ms HMR.
// SWC: 3.2s initial, 45ms HMR.
// The difference is Rust vs JavaScript.
// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// The plugin is a tree transformation.
// The tool handles parsing and codegen.
// The language determines the trade-off.
module.exports = function examplePlugin() {
return {
visitor: {
JSXElement(path) {
// Transform a JSX element.
},
},
};
};
The ten parts build the Babel plugin structure, the React Compiler configuration, the SWC configuration, and the AST inspection workflow. The pattern is consistent: the plugin is a visitor that mutates the tree, and the tool handles the parsing and code generation around it.
Quick Reference
Babel Plugin Hooks
| Hook | When | Purpose |
|---|---|---|
pre(file) | Before traversal | Initialize state |
visitor | During traversal | Transform nodes |
post(file) | After traversal | Cleanup, logging |
Path Methods
| Method | Effect |
|---|---|
path.node | Access the AST node |
path.parent | Access the parent node |
path.replaceWith(node) | Replace the node |
path.remove() | Remove the node |
path.skip() | Skip children |
path.scope | Access scope bindings |
React Compiler Options
| Option | Values | Purpose |
|---|---|---|
sources | Function → 'all', 'none', 'opt-in', 'opt-out' | File selection |
mode | 'all_features', 'no_inferred_memo' | Compilation mode |
environment.validateHooksUsage | boolean | Rules of hooks check |
enableReanimatedCheck | boolean | Reanimated compatibility |
Babel vs SWC
| Aspect | Babel | SWC |
|---|---|---|
| Language | JavaScript | Rust |
| Plugin language | JavaScript | Rust |
| Speed | Baseline | ~14–18x faster |
| Plugin ecosystem | Largest | Smaller |
| Type checking | Via tsc | Via tsc |
| Default in Next.js | No | Yes (12+) |
Best Practices
✅ Do This:
// Use path.replaceWith for node replacement.
path.replaceWith(t.identifier('newName')); // ✅
// Use pre/post for state that spans nodes.
pre() { this.count = 0; } // ✅
// Use the React Compiler opt-in for new features.
sources: (f) => f.includes('features') ? 'opt-in' : 'all' // ✅
// Use SWC for speed when the plugin ecosystem allows.
runtime: "automatic" // in .swcrc // ✅
// Use astexplore-mcp to inspect the AST before writing the plugin.
// npx astexplore-mcp parse "code" --parser babel // ✅
❌ Don’t Do This:
// Don't mutate the AST by string manipulation.
path.node.name = path.node.name.replace(...); // fragile // ❌
// Don't skip the React import when using classic runtime.
// runtime: "classic" requires React in scope. // ❌
// Don't assume SWC has every Babel plugin.
// Check the plugin ecosystem before migrating. // ❌
// Don't write a SWC plugin in JavaScript.
// SWC plugins are Rust dynamic libraries. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Babel plugin does not run | Not added to plugins array | Add the plugin path |
| Identifier rename breaks scope | Global rename without scope check | Use path.scope.hasBinding |
| React Compiler skips component | Opt-in mode without 'use memo' | Add the directive or change sources |
| SWC plugin not found | Not compiled or wrong path | Build with cargo build --release, use absolute path |
| Type errors not caught | SWC strips but does not check | Run tsc --noEmit separately |
| JSX transform missing | Runtime misconfigured | Set runtime: "automatic" for React 17+ |
Real-World Examples
1. Rename Identifier
Identifier(path) { if (path.node.name === 'old') path.node.name = 'new'; }
2. Remove Console
CallExpression(path) { if (path.node.callee.name === 'console') path.remove(); }
3. React Compiler Opt-In
['babel-plugin-react-compiler', { sources: () => 'opt-in' }]
4. Use Memo Directive
function Component() { 'use memo'; return <div />; }
5. SWC React Config
{ "jsc": { "transform": { "react": { "runtime": "automatic" } } } }
6. SWC Plugin Build
cargo build --release # produces target/release/libplugin.so
7. AST Inspection
npx astexplore-mcp parse "const x = 1" --parser babel
8. Pre Hook
pre() { this.startTime = Date.now(); }
9. Post Hook
post() { console.log(Date.now() - this.startTime); }
10. Babel Config
module.exports = { plugins: ['./my-plugin.js'] };
Visual
The Babel Plugin Pipeline
┌─────────────────────────────────────────────────────────┐
│ SOURCE CODE │
│ │ │
│ ▼ @babel/parser │
│ AST │
│ │ │
│ ▼ plugin.pre() │
│ INITIALIZED STATE │
│ │ │
│ ▼ traversal │
│ VISITOR FUNCTIONS (Identifier, CallExpression, ...) │
│ path.node, path.replaceWith, path.remove │
│ │ │
│ ▼ plugin.post() │
│ FINAL AST │
│ │ │
│ ▼ @babel/generator │
│ TRANSFORMED CODE │
└─────────────────────────────────────────────────────────┘
The React Compiler Phases
┌─────────────────────────────────────────────────────────┐
│ 1. LOWER → HIR (control-flow graph) │
│ 2. SSA conversion (single assignment, phi nodes) │
│ 3. CONSTANT PROPAGATION, DEAD CODE ELIMINATION │
│ 4. MUTATION AND ALIASING INFERENCE │
│ 5. REACTIVE SCOPES (group co-mutating variables) │
│ 6. HIR → REACTIVE FUNCTION (tree structure) │
│ 7. CODEGEN (Babel AST with memoization) │
│ │
│ Over 55 passes across seven phases. │
│ The output is memoized JavaScript. │
└─────────────────────────────────────────────────────────┘
The Babel vs SWC Speed Difference
┌─────────────────────────────────────────────────────────┐
│ MEDIUM REACT PROJECT (~500 components, ~50k LOC) │
│ │
│ TASK BABEL SWC SPEEDUP │
│ Initial compile 45s 3.2s ~14x │
│ Hot reload 800ms 45ms ~18x │
│ Production build 2m15s 8.5s ~16x │
│ │
│ Reason: Rust native execution vs Node.js │
│ Trade-off: plugin ecosystem and Rust requirement │
└─────────────────────────────────────────────────────────┘
The SWC Plugin Boundary
┌─────────────────────────────────────────────────────────┐
│ .swcrc │
│ plugins: ["/absolute/path/libmy_plugin.so"] │
│ │ │
│ ▼ SWC loads the dynamic library │
│ Rust plugin │
│ impl VisitMut for MyPlugin │
│ visit_mut_identifier(&mut Ident) │
│ │ │
│ ▼ AST mutated in place │
│ SWC generates the output │
│ │
│ The plugin is Rust. The config is JSON. │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Babel plugin | Function returning { pre, visitor, post } |
| Visitor key | AST node type (Identifier, JSXElement) |
| Path object | node, parent, scope, replaceWith |
| React Compiler | babel-plugin-react-compiler |
| Compiler pipeline | 7 phases, 55+ passes |
| Opt-in directive | 'use memo' |
| SWC config | .swcrc with jsc.transform.react |
| SWC plugin language | Rust (VisitMut trait) |
| SWC speed | ~14–18x faster than Babel |
| AST inspection | AST Explorer, astexplore-mcp |
Key takeaways:
- A Babel plugin is a visitor that mutates the AST. The tool parses the source and generates the output; the plugin’s only job is to transform the tree at the nodes it cares about .
- The
pathobject is the primary interface. It provides the node, the parent, the scope, and the mutation methods.path.scope.hasBindingis the correct way to check whether an identifier refers to a local variable. - The React Compiler is the most consequential Babel plugin. It analyzes components through a seven-phase pipeline and inserts memoization. The
sourcesoption controls which files it processes, and'use memo'is the opt-in directive . - SWC is the performance alternative. It is 14–18x faster than Babel because it is written in Rust. The plugin ecosystem is smaller, and TypeScript type checking still requires
tsc --noEmit. - SWC plugins are Rust dynamic libraries. The
.swcrcpoints to the compiled.so/.dylib/.dll, and the plugin implementsVisitMut. JavaScript cannot be used for the core transformation logic . - The React runtime setting determines the JSX output.
runtime: "automatic"uses the React 17+ transform and does not require a React import.runtime: "classic"uses the older transform and does. The plugin must account for the runtime the project uses. - AST inspection is the first step. AST Explorer and
astexplore-mcpshow the tree that the plugin will traverse. Writing a plugin without inspecting the AST is guessing at the node structure.
Remember: An AST transformation is a tree rewrite. The Babel or SWC plugin receives the tree, visits the nodes it cares about, and mutates them. The tool handles the parsing and code generation around the plugin, so the plugin does not need to know how the source is tokenized or how the output is formatted. The choice between Babel and SWC is a choice of language and speed: Babel is JavaScript and accessible, SWC is Rust and fast. The React Compiler is the proof that the plugin model can express sophisticated analysis, and the performance numbers are the proof that the language matters. Write the plugin, inspect the AST, and measure the transformation before trusting it.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!