Every React Native developer runs npx react-native start hundreds of times. But most have no idea what's actually happening between hitting enter and seeing "Metro waiting on port 8081." Understanding Metro's internals won't just satisfy your curiosity — it'll make you faster at debugging slow builds, broken imports, and mysterious cache issues.

What is Metro?

Metro is React Native's dedicated JavaScript bundler. It's not Webpack, not Vite, not esbuild — it's purpose-built by Meta for the unique constraints of mobile development. Three things make it different:

Speed Sub-second incremental builds 📦 Modularity Pluggable resolver & transformer 🔄 Hot Reload Fast Refresh without losing state

The Big Picture: 4 Phases

When you run npx react-native start, Metro goes through four phases before it starts serving your app. Every startup, every time.

Phase 1 Initialize Phase 2 Crawl Phase 3 Build Graph Phase 4 Serve

Phase 1: Initialization

Metro starts by loading your configuration. It reads metro.config.js, resolves the config chain (your overrides merged with defaults), sets up the resolver, and boots the HTTP server. This is where your custom resolver configs, blockList patterns, and extraNodeModules mappings get processed.

// metro.config.js — this is where Metro starts
module.exports = {
  resolver: {
    blockList: [/\.git\/.*/],
    extraNodeModules: {
      '@shared': path.resolve(__dirname, '../shared')
    }
  },
  transformer: {
    babelTransformerPath: require.resolve('react-native-svg-transformer')
  }
};

Phase 2: File System Crawling

This is where Metro discovers your project. It crawls your entire file system — every .js, .ts, .tsx file in your project and node_modules. The result is the haste map — a lookup table of every file, its path, and a content hash.

project-root/ src/ node_modules/ assets/ App.tsx → a3f2c1 Home.tsx → b7d9e4 Watchman (fast) or Node fs (fallback)

Metro uses Watchman (Meta's file-watching service) for fast crawling. If Watchman isn't installed, it falls back to Node's native fs module — significantly slower. This is why installing Watchman is one of the first performance wins for any React Native project.

Phase 3: Module Graph & Dependency Resolution

Now Metro builds the dependency graph. Starting from your entry point (index.js), it traces every import and require statement, recursively building a directed graph of every module your app depends on.

index.js App.tsx config.ts HomeScreen Navigation react-native Every import traced recursively from entry point

This graph is the core data structure Metro operates on. Every future operation — bundling, transformation, hot reload — is a traversal of this graph.

Phase 4: Transformation Pipeline

When your app requests a bundle (by hitting localhost:8081/index.bundle), Metro transforms every module in the graph through a pipeline:

Your .tsx file Raw source code Babel Transform JSX → JS, TS → JS Inline Requires Lazy module loading Hermes Bytecode Compiled for device 📱

Your raw .tsx file goes through Babel (JSX and TypeScript to plain JavaScript), inline requires optimization (lazy module loading for faster startup), and finally Hermes bytecode compilation (pre-compiled for the device). The output is a single bundle file that your app loads.

The Bundle Request Flow

Here's what happens when your React Native app boots and requests its JavaScript bundle:

RN App boots requests bundle Metro Server :8081/index.bundle Walk Graph resolve deps Transform all modules Bundle → device

The app boots, requests localhost:8081/index.bundle, Metro walks the dependency graph, transforms every module in the pipeline, serializes them into a single bundle, and sends it back. On subsequent requests, Metro uses cached transforms — only re-transforming files whose content hash has changed.

Fast Refresh Internals

This is Metro's killer feature. When you save a file, Metro doesn't rebuild the entire bundle. It sends a delta — only the changed modules — to the running app.

You save file Cmd+S Watchman detects change Re-transform only changed file Delta sent via WebSocket Hot inject state preserved

The cycle: you save → Watchman detects the file change → Metro re-transforms only the changed module → sends a delta over WebSocket → the React Native runtime hot-injects the updated module — without losing component state. This is why you can edit styles and see them update instantly without navigating back to the screen.

Power-User Tips

  • --reset-cache — When things feel wrong and you can't explain why, run npx react-native start --reset-cache. It nukes Metro's transform cache and the haste map, forcing a full rebuild. This fixes most "works on my machine" issues.
  • Custom resolvers — Use resolver.extraNodeModules in metro.config.js for monorepo setups where packages live outside the project root.
  • blockList — Use regex patterns to exclude directories from Metro's crawl. Essential for monorepos where you don't want Metro crawling sibling packages.

Final Thoughts

Metro isn't a black box. It's a pipeline: Initialize → Crawl → Build Graph → Serve. Every performance issue, every weird import error, every cache problem traces back to one of these four phases. Understanding which phase is responsible for your problem is 80% of fixing it.

Now you know what really happens when you hit enter.