skip to content

Transformer & Babel

Metro runs every file through a Babel transformer in worker processes and can defer module execution with inline requires. Interviewers ask what it does and what web bundlers do that it skips.

on this pageshow

explore

questions

5

In React Native's Metro bundler, what happens to each source file during the transformation stage, and why does it run in worker processes?

level: juniorimportance: must knowfreq 48%

answer

  1. resolve, transform, serialize
  2. one file in, one module out
  3. Babel via babelTransformerPath
  4. dependencies collected, then __d() wrapper
  5. output depends on file plus config only

basics

~20 s

Metro sends each file, on its own, through a transformer in a pool of worker processes: Babel compiles JSX, TypeScript and newer syntax, then Metro records the dependencies and wraps it as a module, so files transform in parallel.

solid answer

~40 s

Metro bundles in three stages: resolution, transformation and serialization, with the first two interleaved as it walks the graph from the entry file. In transformation, each file goes to the transform worker (`metro-transform-worker` by default), which calls the Babel transformer named by `transformer.babelTransformerPath`; in a bare React Native app that is `@react-native/metro-babel-transformer`, which applies the project's `babel.config.js` or falls back to `@react-native/babel-preset`. Metro then runs its own passes: inlining `__DEV__`, optional inline requires, constant folding in production builds. It collects the `require` and `import` dependencies it found, wraps the code in a `__d(...)` module factory and minifies when asked. Because a file's output depends only on its own source and the config, files can be spread across worker processes and each result can be cached separately.

code

javascript · 4 lines
javascript
// babel.config.js in a bare React Native 0.87 app
module.exports = {
  presets: ['module:@react-native/babel-preset'],
};

go deeper

for a junior

Name the three stages, resolution, transformation and serialization, and say that Babel compiles every file separately before Metro wraps it as a module.

for a middle

Explain the chain transform worker, babelTransformerPath, babel.config.js, and why per-file isolation is what allows parallel workers and a per-file cache.

for a senior

Use the pipeline to diagnose: a library that fails only in the bundle usually needs a Babel plugin or preset change, and a config change needs a Metro restart to take effect.

for a principal

Weigh the design: per-file isolation buys fast incremental rebuilds in development at the cost of whole-program optimisations such as cross-module tree shaking.

## The three stages of a Metro build **Metro** is the JavaScript bundler that React Native and Expo use. It starts from an entry file (for example `index.js`) and produces one JavaScript bundle that the app's engine loads. The work happens in three stages: 1. **Resolution**: for every `import` or `require`, find the file on disk that the specifier points to. 2. **Transformation**: turn each file's source into plain JavaScript the engine can run, packaged as a module. 3. **Serialization**: put the transformed modules, the module runtime and the polyfills together into the bundle. Resolution and transformation are interleaved: Metro only learns a file's dependencies after transforming it, so it transforms, reads the dependency list, resolves those specifiers, and transforms the new files in turn until the graph is complete. ## What happens to one file A file travels through the **transform worker** (`transformerPath`, default `metro-transform-worker`). For a JavaScript or TypeScript file it does roughly this: 1. **Babel pass.** The worker loads the module named by `transformer.babelTransformerPath` and asks it to compile the file. In a bare React Native 0.87 app that module is `@react-native/metro-babel-transformer`: it looks for `.babelrc`, `.babelrc.js` or `babel.config.js` in the project root and extends it, and if none exists it applies `@react-native/babel-preset` directly. That pass removes JSX, TypeScript and Flow syntax and lowers newer language features for Hermes. 2. **Metro's own passes.** It inlines `__DEV__`, applies **inline requires** when `getTransformOptions` enables them, and in production builds runs a **constant-folding** pass so that branches such as `if (false) { … }` disappear. 3. **Dependency collection.** It records every `require()`, `import` and dynamic `import()` it still finds. That list is what drives the next round of resolution. 4. **Wrapping.** The code is wrapped in a `__d(function (global, require, …) { … })` module factory, so the module runs only when first required. 5. **Minification.** When the build asks for it, the minifier configured by `minifierPath` (default `metro-minify-terser`) runs on the file. Every file in the graph goes through this, including files in `node_modules`. That matters more than it sounds: the `react-native` package itself ships Flow-typed source, and many libraries ship JSX, so they only run because Metro compiles them on the way in. ## Why the work runs in worker processes The key property is **isolation**: the output for a file depends only on that file's contents and the transform configuration, never on another file. So: - Files can be transformed **in parallel**. Metro keeps a pool of workers, sized by `maxWorkers`, whose default is derived from the machine's core count. Plain Metro uses child processes; Expo's default config switches to worker threads, and the isolation is the same. - A result can be **cached per file**. On the next start Metro reuses cached output for unchanged files and re-transforms only what changed. - An edit during development re-transforms **one file**, not the whole project, which keeps rebuilds fast. Babel is CPU-bound and single-threaded inside one process, so separate processes are how Metro uses more than one core. The flip side of isolation is that a Babel plugin only ever sees one file at a time; nothing in this stage can reason about the whole app. ## Where you configure each piece | Piece | Config | React Native default | |---|---|---| | The worker that runs the pipeline | `transformerPath` | `metro-transform-worker` | | The module that runs Babel | `transformer.babelTransformerPath` | `@react-native/metro-babel-transformer` | | Presets and plugins | `babel.config.js` | preset `module:@react-native/babel-preset` | | Inline requires and import handling | `transformer.getTransformOptions` | `inlineRequires: true` | | Minifier | `transformer.minifierPath` | `metro-minify-terser` | Expo's `getDefaultConfig` from `expo/metro-config` swaps in its own worker and Babel transformer so that `babel-preset-expo` is always applied, and returns `inlineRequires: false`. ## A worked example Take `src/Greeting.tsx`, which imports `Text` from `react-native` and returns `<Text>Hello {name}</Text>`. After the Babel pass the type annotations are gone and the JSX has become plain function calls. Metro then notes two dependencies, `react/jsx-runtime` and `react-native`, and resolves each to a file. Finally the whole body sits inside a `__d(...)` factory with a numeric module id. Nothing about `Greeting.tsx` needed any other file to be compiled first, which is the whole point. ## What this stage does not do - It does **not** touch native code: Kotlin, Java, Swift and Objective-C are compiled by Gradle and Xcode, not Metro. - It does **not** combine files: that is serialization's job. - It does **not** analyse the whole program, which is why Metro has no cross-module tree shaking. A good junior answer names the three stages, says that Babel runs per file inside workers, and explains that per-file isolation is what makes both parallelism and caching possible.

  • Does Metro transform files inside node_modules too?
    Yes. Every module that ends up in the graph goes through the transformer, including dependencies. The `react-native` package itself ships Flow-typed source, and many React Native libraries ship JSX, so they only run because Metro compiles them. It also means a misconfigured Babel plugin can break a dependency, not only your own files.
  • Where does the Babel configuration come from in a bare React Native app?
    `@react-native/metro-babel-transformer` looks in the project root for `.babelrc`, `.babelrc.js` or `babel.config.js` and extends the first one it finds. If there is none, it applies `@react-native/babel-preset` directly. The template ships a `babel.config.js` whose only preset is `module:@react-native/babel-preset`.
  • Why can a Babel plugin in a React Native project not see which exports other files use?
    Metro hands the Babel transformer one file at a time, inside a worker, and caches the result against that file alone. A plugin that needed other files would make the output depend on them, which would break both parallel transformation and per-file caching.

A print shop with several presses: each page is typeset on whichever press is free, and because a page never depends on another page, a reprint only resets the one page that changed. Binding the book is a separate step at the end.

saying these in an interview costs you the question

  • Metro compiles the whole project in one pass, like a single compiler run
  • Babel runs once on the finished bundle after all files are joined
  • Metro's transformer also compiles the Kotlin and Swift native code
  • Files in node_modules are copied into the bundle without transformation
  • A Babel plugin in Metro can inspect how other files use a module
open as a page

In a React Native project, what does babel.config.js control, and how does babel-preset-expo differ from @react-native/babel-preset?

level: middleimportance: should knowfreq 40%

basics

~10 s

babel.config.js lists the Babel presets and plugins Metro applies to every file. Bare apps use @react-native/babel-preset; Expo apps use babel-preset-expo, which extends it with Expo-specific transforms, and the file is optional in Expo.

open as a page

Why does React Native's Metro bundler not tree-shake unused exports the way many web bundlers do, and what dead code does it still remove?

level: middleimportance: should knowfreq 30%

basics

~20 s

Metro transforms and caches each file on its own, so no step looks at which exports other files use: a reached module is bundled whole. It still removes dead branches per file by inlining DEV and Platform.OS, constant folding and minification.

open as a page

In a React Native app bundled by Metro, how do you import SVG icon files as components, and why does babelTransformerPath matter?

level: middleimportance: should knowfreq 35%

basics

~20 s

Metro treats .svg as an image asset by default, and core React Native cannot draw SVG. Point transformer.babelTransformerPath at an SVG transformer that turns each .svg into a react-native-svg component, and move svg from assetExts to sourceExts.

open as a page

In Metro, what does the inlineRequires option returned by getTransformOptions do, and how can enabling it break a module that relies on side effects?

level: seniorimportance: should knowfreq 25%

basics

~20 s

inlineRequires makes Metro move module-level require() bindings to where they are used, so a module is evaluated on first use instead of at load. A module whose side effects others depend on can then run late, out of order, or never.

open as a page