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:
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: 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.
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.
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 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:
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.
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, runnpx 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.extraNodeModulesinmetro.config.jsfor 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.
Related
- Loop Engineering Explained — how loops are replacing prompting
- Context Engineering Explained — why context matters more than prompts
- The N×M Problem — why AI integrations were broken before MCP