In React Native's Metro bundler, what happens to each source file during the transformation stage, and why does it run in worker processes?
answer
- resolve, transform, serialize
- one file in, one module out
- Babel via babelTransformerPath
- dependencies collected, then __d() wrapper
- output depends on file plus config only
basics
~20 sMetro 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 sMetro 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// babel.config.js in a bare React Native 0.87 app
module.exports = {
presets: ['module:@react-native/babel-preset'],
};go deeper
Name the three stages, resolution, transformation and serialization, and say that Babel compiles every file separately before Metro wraps it as a module.
Explain the chain transform worker, babelTransformerPath, babel.config.js, and why per-file isolation is what allows parallel workers and a per-file cache.
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.
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