skip to content

Metro Bundler

Metro turns React Native source into one bundle: it resolves imports per platform, transforms each file with Babel and caches the results. Interviewers probe resolution and stale-cache bugs.

on this pageshow

explore

questions

19

In a React Native project, when should you reset Metro's cache, which command does it, and what does the reset actually clear?

level: juniorimportance: must knowfreq 60%

answer

  1. stale output after config or dependency changes
  2. npx react-native start --reset-cache
  3. npx expo start --clear (-c)
  4. transform cache plus file map cache
  5. not node_modules, not native builds

basics

~20 s

Reset Metro's cache when bundles show old code or config after a Babel, dependency or branch change. npx react-native start --reset-cache or npx expo start --clear discards the transform and file map caches, not node_modules or native builds.

solid answer

~50 s

Metro caches every transformed file and keeps a cached **file map** of the project, so a warm start skips most work. When the cache no longer matches reality, for example after editing `babel.config.js` in a bare app, upgrading a Babel plugin or switching branches while Metro was running, you see old code or phantom errors. The fix is a clean start: `npx react-native start --reset-cache` in a React Native CLI project, `npx expo start --clear` (or `-c`) in Expo, or `resetCache: true` in the Metro config. It resets the transformer cache in `cacheStores` and the file map cache, so the first bundle afterwards is slow. It does not reinstall `node_modules`, clear Watchman's own watches, touch Gradle or Xcode build output, or change what is installed on the device. Use it deliberately rather than as a ritual on every start.

code

bash · 10 lines
bash
# React Native CLI project
npx react-native start --reset-cache

# Expo project
npx expo start --clear

# Metro's documented escalation when a reset is not enough
watchman watch-del-all
rm -rf node_modules && npm install
rm -rf "${TMPDIR:-/tmp}"/metro-*

go deeper

for a junior

Know the reset command for your project type and recognise the symptom: old code or phantom errors after a Babel, dependency or branch change.

for a middle

Explain that a reset clears the transform cache and the file map cache, and what it leaves alone: node_modules, Watchman and native builds.

for a senior

Treat repeated resets as a signal that an input is missing from the cache key, and fix it with cacheVersion or config rather than a habit.

for a principal

Make cache behaviour predictable for the whole team, so resets become rare and are not part of anyone's daily workflow.

## What Metro caches **Metro**, the bundler React Native and Expo use, keeps two kinds of state between runs: - the **transform cache**: the compiled output of every file, stored by default by a `FileStore` under the operating system's temporary directory (`os.tmpdir()/metro-cache`). A warm start reuses it for every unchanged file; - the **file map**: Metro's index of every file it can see in the project, with metadata, also persisted (by default under `os.tmpdir()`, configurable with `fileMapCacheDirectory`) so a restart does not need to crawl the whole tree from scratch. Both make development fast. Both can go stale when something changes that Metro's cache key or file watcher did not notice. ## When a reset is the right move 1. **After changing Babel configuration in a bare React Native app.** The React Native Babel transformer's cache key covers the preset version, not your `babel.config.js`, so an edited plugin list can keep serving old output. 2. **After upgrading or adding a Babel plugin** whose output depends on something outside the file being compiled. 3. **When a Babel plugin inlines environment variables** and you changed the variable: the file content did not change, so the key did not either. 4. **After a large branch switch or dependency change** that left errors which do not match the files on disk. 5. **When the bundle clearly contains code you have already deleted.** Most other changes, including edits to source files and new files, are handled automatically: the cache key includes each file's content hash, and the watcher updates the file map. ## The commands | Project | Command | Notes | |---|---|---| | React Native CLI | `npx react-native start --reset-cache` | one run only | | Expo | `npx expo start --clear` | `-c` is the short form; `--reset-cache` is accepted as an alias | | Any Metro config | `resetCache: true` | resets on every start; do not leave it committed | | Release bundle | `npx react-native bundle --reset-cache …` | for a one-off clean bundle | Metro's docs list the escalation path when a reset is not enough: clear Watchman's watches with `watchman watch-del-all`, delete and reinstall `node_modules`, then reset the cache again, and as a last resort remove Metro's temporary files (`rm -rf ${TMPDIR:-/tmp}/metro-*`). ## Read the symptom before resetting - **Old code still runs** after a Babel or plugin change: stale transform output, and a reset fixes it. - **A resolution error for a file that exists**: a stale file map, and a restart or reset fixes it. - **A resolution error for a package**: usually a missing install, which a reset cannot fix. - **A native crash or a missing native module**: a native build problem, outside Metro entirely. Matching the symptom first keeps the reset for the cases it can actually solve. ## What a reset does not do - It does **not** reinstall dependencies. A package missing from `node_modules` stays missing. - It does **not** clear Watchman's state; that is `watchman watch-del-all`. - It does **not** clean native builds: Gradle and Xcode keep their own caches, and a new native module still needs a native rebuild. - It does **not** change the app installed on a device beyond serving it a fresh bundle on the next load. ## Costs and habits A reset forces every file to be transformed again, so the next bundle takes much longer. Teams that reset on every start lose the main benefit of the cache and hide the real cause of staleness. Better habits: - reset once after the kinds of change listed above; - when you find yourself resetting after every change of a particular setting, make that input part of the key instead, for example by deriving `cacheVersion` from it; - never commit `resetCache: true`. ## What interviewers listen for A junior answer names the command for the project type and the typical trigger (old code still running after a Babel or dependency change). A stronger answer explains that the reset clears both the transform cache and the file map, and knows what it leaves alone, so it does not reach for `--reset-cache` to fix a missing dependency or a native module that was never built.

  • Why is committing resetCache: true to the Metro config a bad idea?
    It discards the transform cache and file map on every start, so every developer and CI run pays for a full transformation each time. It also hides the underlying problem, an input missing from the cache key, instead of fixing it, for example with `cacheVersion`.
  • A newly installed library with native code crashes the app, and --reset-cache does not help. Why?
    The reset only affects Metro's JavaScript caches. Native code must be compiled into the app, so the app needs a native rebuild, and on iOS usually a CocoaPods install first. No bundler cache can supply native code that is not in the binary.

A restaurant's prep fridge: pre-cooked components save time on every order, but if the recipe changed and nobody relabelled the containers, the kitchen keeps serving the old dish until someone empties the fridge and cooks fresh.

saying these in an interview costs you the question

  • --reset-cache also reinstalls node_modules
  • Resetting the cache on every start is harmless best practice
  • Metro never needs a reset because it notices every change
  • --reset-cache rebuilds native modules for the app
  • Clearing Metro's cache also clears Watchman's watches
open as a page

In a React Native app, how does Metro decide whether an imported file is source code or an asset, and what does require('./logo.png') return?

level: juniorimportance: must knowfreq 50%

basics

~20 s

Metro classifies a file by its extension: resolver.sourceExts (js, jsx, json, ts, tsx by default) are transformed and bundled as code, resolver.assetExts (png, jpg, ttf, mp4 and more) are copied as assets. require('./logo.png') returns a numeric asset registry ID that Image resolves.

open as a page

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%

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.

open as a page

After switching git branches, a React Native app fails with Metro's 'Unable to resolve module' error. How do you triage it?

level: middleimportance: must knowfreq 55%

basics

~20 s

Read which module failed and which file imported it. A missing package usually means node_modules still matches the old branch, so reinstall. A missing relative file is a real code change. If the file exists, restart Metro, then reset caches.

open as a page

In a React Native metro.config.js, how do getDefaultConfig and mergeConfig work together, and how do you add an extra asset extension such as .glb safely?

level: middleimportance: must knowfreq 52%

basics

~20 s

getDefaultConfig returns React Native's complete Metro defaults and mergeConfig overlays your changes section by section. Arrays are replaced, not appended, so add .glb by spreading the default assetExts plus 'glb', never by passing ['glb'] alone.

open as a page

In a monorepo, a React Native app importing a shared UI package crashes with an invalid hook call; how does Metro resolution cause it, and how do you fix it?

level: seniorimportance: must knowfreq 44%

basics

~20 s

Metro resolves react from each importing file's own folder upwards, so a copy nested in packages/ui/node_modules is bundled beside the app's copy. Make react a peer dependency and dedupe it, or pin react and react-native in resolveRequest or blockList the nested copy.

open as a page

In a React Native project's metro.config.js, what do the resolver, transformer, serializer and server sections each control?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Metro's config mirrors its pipeline: resolver decides which file an import points to, transformer compiles each file, serializer assembles modules into the bundle, and server configures the development server that serves it, on port 8081 by default.

open as a page

How does Metro build the cache key for a transformed file, and which changes can it miss so that stale output is served?

level: middleimportance: should knowfreq 32%

basics

~20 s

Metro's key combines each file's content hash and relative path, the bundle's transform options, and Metro's version, cacheVersion and transformer config. It misses inputs outside those, like environment variables a Babel plugin reads, and bare React Native's babel.config.js edits.

open as a page

How does getDefaultConfig from expo/metro-config differ from the one in @react-native/metro-config, and which should a project extend?

level: middleimportance: should knowfreq 38%

basics

~20 s

Extend the package that matches the CLI running Metro. @react-native/metro-config adds React Native's defaults for the React Native CLI; expo/metro-config builds on Metro's defaults with Expo's transformer, serializers, extra extensions and monorepo detection, and Expo CLI expects it.

open as a page

In Metro, what do watchFolders, nodeModulesPaths and extraNodeModules each do, and in what order does resolution consult them?

level: middleimportance: should knowfreq 32%

basics

~20 s

watchFolders makes files outside projectRoot visible to Metro at all. For a package import, Metro first walks node_modules up from the importing file, then tries each nodeModulesPaths entry, and only then maps the name through extraNodeModules.

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

How would you configure Metro's cacheStores so CI and a React Native team reuse transform results instead of re-transforming every file on each build?

level: seniorimportance: should knowfreq 18%

basics

~20 s

Point a FileStore at a directory CI persists between runs, and for a team add a remote store: CI writes with HttpStore, developers read with HttpGetStore, listed after the local FileStore. Metro's portable keys make shared entries safe to reuse.

open as a page

A React Native release build on CI is killed for running out of memory while Metro bundles JavaScript. How is Metro's maxWorkers involved, and how would you tune it?

level: seniorimportance: should knowfreq 20%

basics

~20 s

Metro runs transformation in parallel workers sized by maxWorkers, which defaults from os.availableParallelism(). A CI machine with many cores but little RAM runs out of memory. Lower maxWorkers in the config or pass --max-workers to the bundle step.

open as a page

Since Metro resolves package.json exports by default, why might a React Native library's platform-specific file stop loading, and how do condition names help?

level: seniorimportance: should knowfreq 28%

basics

~20 s

With unstable_enablePackageExports on by default since Metro 0.82 (React Native 0.79), a subpath matched in exports resolves to its exact target, without .ios/.android or extension expansion. Libraries must route platforms through conditions: React Native asserts react-native, and Metro always asserts default plus import or require.

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

In Metro 0.87, which config file names and formats can a React Native project use, and how does Metro determine projectRoot?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

Metro 0.87 finds metro.config.js/.cjs/.mjs, then .ts/.cts/.mts, then .json, then the same names under .config/, then a metro key in package.json; --config overrides the search. TypeScript loads through Node's type stripping, YAML is gone, and projectRoot defaults to the config file's folder.

open as a page

In Metro, why can Button.js be loaded on iOS even though Button.ios.tsx exists next to it?

level: middleimportance: nice to knowfreq 16%

basics

~10 s

Metro's resolver loops over sourceExts in order and, for each extension, tries .ios, then .native, then the plain name. With the default order js before tsx, Button.js matches before Button.ios.tsx is ever tried.

open as a page