InkdownInkdown
Start writing

Study

69 files·11 subfolders

Shared Workspace

Study
core
Revision w/ Whiteboard

metro-bundler-deep-dive

Shared from "Study" on Inkdown

Metro Bundler: The Complete Core-Level Guide

Audience: Engineers who want the full mental model — from first principles to internals.
Scope: General. Applies to any React Native / Metro project. No app-specific assumptions.
Last updated: June 2026


Table of contents

  1. The one-sentence answer
  2. What Metro is — and what it is not
  3. Why Metro exists
  4. Where Metro sits in the React Native stack
  5. Metro’s internal architecture (packages)
  6. The dependency graph — Metro’s real data structure
CN Basics - 1
CN Basics - 2
DNS
Event loop
programming-language-concepts.md
zero-language-explanation.md
DB
Quick
databases-deep-dive.md
01-introduction.md
02-relational-databases.md
03-database-design.md
04-indexing.md
05-transactions-acid.md
06-nosql-databases.md
07-query-optimization.md
08-replication-ha.md
09-sharding-partitioning.md
10-caching-strategies.md
11-cap-theorem.md
12-connection-pooling.md
13-backup-recovery.md
14-monitoring.md
15-database-selection.md
README.md
JS
core topics
Event loop
Merlin Backend
01-Orchestration.md
02-DeepResearch.md
03-Search.md
04-Scraping.md
05-Streaming.md
06-MultiProviderLLM.md
07-MemoryAndContext.md
08-ErrorHandling.md
09-RateLimiting.md
10-TaskQueue.md
11-SecurityAndAuth.md
Orchestration-2nd-draft
Mobile
Build Alternative
Bundling
metro-bundler-deep-dive.md
OpenAI Agents Python
00_OVERVIEW.md
01_AGENT_SYSTEM.md
02_RUNNER_SYSTEM.md
03_TOOL_SYSTEM.md
04_ITEMS_SYSTEM.md
05_GUARDRAILS.md
06_HANDOFFS.md
07_MEMORY_SESSIONS.md
08_MODEL_PROVIDERS.md
09_SANDBOX_SYSTEM.md
10_TRACING.md
11_RUN_STATE.md
12_CONTEXT.md
13_LIFECYCLE_HOOKS.md
14_CONFIGURATION.md
15_ERROR_HANDLING.md
16_STREAMING.md
17_EXTENSIONS.md
18_MCP_INTEGRATION.md
19_BEST_PRACTICES.md
20_ARCHITECTURE_PATTERNS.md
opencode-study
context-handling
core
Python
Alembic
Basics
sqlalchemy - fastapi
SQLAlchemy overview
tweets
system_design_for_agentic_apps.md
Agent Loop
  • The three core phases
  • Incremental bundling: DeltaBundler
  • The filesystem index: metro-file-map
  • Resolution — how imports become file paths
  • Transformation — how source becomes runnable JS
  • Serialization — how the graph becomes a bundle
  • The Metro runtime — how bundles actually execute
  • Bundle formats: plain, indexed RAM, file RAM
  • The dev server and HTTP API
  • HMR, live reload, and Fast Refresh
  • Caching — every layer explained
  • File watching: Watchman and rebuild triggers
  • Platform-specific resolution
  • The asset pipeline
  • Source maps
  • Development vs production builds
  • Code splitting and dynamic import
  • Hermes and the post-Metro pipeline
  • Configuration — the full surface
  • Monorepos and workspaces
  • Custom resolvers and transformers
  • Tree shaking, dead code, and Metro’s limits
  • Debugging Metro failures
  • Metro vs Webpack vs Vite vs Rollup
  • End-to-end walkthrough: one edit, one rebuild
  • Glossary
  • Further reading

  • 1. The one-sentence answer

    Metro is a JavaScript bundler that builds a dependency graph from entry points, transforms every module in that graph, and serializes the result into one or more JavaScript bundles — optimized for incremental rebuilds during React Native development.

    That sentence contains the entire job. Everything else is detail about how each word in that sentence works.


    2. What Metro is — and what it is not

    What Metro is
    RoleDescription
    BundlerCombines many JS/TS files + npm packages into loadable output
    Graph builderTracks every static import / require as edges between modules
    Transformer orchestratorCalls Babel (or custom transformers) per file, in parallel
    Dev serverServes bundles and assets over HTTP during development
    Incremental build systemRebuilds only the subgraph affected by a change

    Metro was created by Meta (Facebook) specifically for React Native. It is the default bundler for React Native and is also used by Expo for native and web targets.

    What Metro is NOT
    ThingWhat actually does it
    React Native itselfNative UI framework + bridge/Fabric
    Hermes / JavaScriptCoreJS engines that execute the bundle Metro produced
    Xcode / GradleNative compilers that build the .app / .apk shell
    BabelA compiler Metro invokes per file — not the bundler
    TypeScript (tsc)Type checker; Metro strips types via Babel, does not type-check
    npm / yarn / pnpm / BunPackage managers that install deps Metro later resolves
    Expo CLIOrchestration layer that starts Metro and configures defaults
    WatchmanA separate file-watching service Metro uses (optional but default)

    Simple analogy: Metro is the factory assembly line. Babel is one machine on that line. Hermes is the engine in the car after the factory ships it.


    3. Why Metro exists

    Webpack and Rollup existed when React Native launched, but they were built for the web. Mobile development has different constraints:

    ConstraintWhy it mattersMetro’s response
    Fast iterationDevs save files hundreds of times per dayIncremental graph updates (DeltaBundler), transform cache
    Large graphsnode_modules + app code = tens of thousands of filesParallel workers, file map pre-indexing
    No native ESM on older RNRN historically used a custom require runtimeNumeric module IDs + __d wrapper
    Platform forksSame import, different file on iOS vs Android vs web.ios.ts, .android.ts, .native.ts, .web.ts
    Asset densityImages at multiple resolutions (@2x, @3x)Asset registry + resolution variants
    Dev on devicePhone pulls bundle over Wi‑Fi from laptopHTTP dev server + HMR WebSocket

    Metro is not “Webpack for mobile.” It is a bundler designed for mobile dev loops from day one.


    4. Where Metro sits in the React Native stack

    Development flow
    Rendering diagram…
    Production flow
    Rendering diagram…

    In development, Metro is a long-running server that keeps a warm graph in memory.
    In production, Metro is a batch compiler that emits artifacts consumed by the native build.


    5. Metro’s internal architecture (packages)

    Metro is not one monolith. It is a monorepo of focused packages, each owning one step:

    Rendering diagram…
    PackageResponsibility
    metroMain orchestrator: runServer, buildGraph, DeltaBundler, dev server
    metro-configConfig merging, defaults, validation
    metro-resolverFull Node-style resolution + RN extensions
    metro-transform-workerPer-file transform in worker threads
    metro-file-mapFilesystem index (replaces older “Haste map” naming in places)
    metro-cachePluggable transform cache stores (file, HTTP)
    metro-runtimerequire polyfill, __d, __r, async bundle loading
    metro-source-mapComposing source maps across transforms
    metro-symbolicateTurn stack traces → original source locations
    metro-babel-transformerDefault Babel integration
    metro-minify-terserProduction minification

    Design principle: Resolver, transformer, and serializer are swappable. You can plug custom implementations via metro.config.js without forking Metro.


    6. The dependency graph — Metro’s real data structure

    Everything Metro does reduces to building and updating a directed graph.

    Nodes and edges
    • Node = one module (a .ts file, an npm package entry, a JSON file, an image asset).
    • Edge = a static dependency from module A to module B (import / require).
    Rendering diagram…
    Module types in the graph
    TypeExampleTreated as
    Source fileApp.tsxTransformed JS module
    Assetlogo.pngAsset registry entry, not executed JS
    Empty moduleBrowser stub for fsZero-byte shim
    Package entryreact-nativeResolved via package.json fields
    Static vs dynamic dependencies
    KindSyntaxGraph behavior
    Static ESMimport X from 'y'Always an edge; target included in graph (or separate chunk on web)
    Static CJSrequire('y')Same as static ESM for graph purposes
    Dynamicimport('./chunk')Async dependency; may become a separate bundle (platform-dependent)
    Dynamic invalidrequire('./' + variable)Cannot resolve at build time; see dynamicDepsInPackages config

    Metro’s graph is built by static analysis of import/require in transformed AST output. It does not execute your code to discover dependencies.

    Cycles (circular imports)

    A → B → A is allowed in JavaScript. Metro does not block cycles. They cause initialization order bugs (one module sees undefined exports from another). Metro can warn about require cycles in dev (configurable via requireCycleIgnorePatterns — by default node_modules cycles are suppressed).

    Graph scope

    A “bundle” is a reachable subgraph from one or more entry points, filtered by platform, dev/prod mode, and serializer options. You do not bundle “the whole repo” — only what is reachable.


    7. The three core phases

    Metro’s official model has three phases. In practice, resolution and transformation are heavily pipelined and parallelized.

    Rendering diagram…
    Phase 1: Resolution

    Question answered: “What file on disk does './Button' or 'react-native' refer to?”

    • Input: bare or relative specifier + importer path + platform
    • Output: absolute real path + type (source / asset / empty)
    Phase 2: Transformation

    Question answered: “What JavaScript should the runtime execute for this file?”

    • Input: raw source + path + options (dev, platform, inlineRequires, etc.)
    • Output: transformed JS + source map + list of static dependencies

    Runs in worker threads (maxWorkers, default ~half of CPU cores). Two separate worker pools exist: one for transforms, one for file-map building.

    Phase 3: Serialization

    Question answered: “How do I pack all modules into bytes the RN runtime understands?”

    • Input: transformed modules with numeric IDs and dependency maps
    • Output: one or more bundle files (plain JS, RAM bundle, or multiple chunks)

    8. Incremental bundling: DeltaBundler

    A full rebuild of a 50,000-module graph on every save would be unusable. Metro’s core innovation for dev speed is incremental graph maintenance via the DeltaBundler.

    Mental model
    Rendering diagram…
    What “delta” means

    When you request a bundle (or HMR update), Metro does not always rewrite the entire output:

    1. Compare current graph to previous graph state.
    2. Identify added, modified, and deleted modules.
    3. Emit a delta — only changed module definitions — for HMR.
    4. For full bundle requests, reuse cached transforms for unchanged modules.
    Cold vs warm start
    Start typeWhat happens
    ColdBuild file map, empty graph, transform everything reachable, populate caches
    WarmFile map cached, graph in memory, transform cache on disk — only delta work

    --reset-cache / resetCache: true forces cold behavior for caches (not necessarily killing the server process).


    9. The filesystem index: metro-file-map

    Before resolving imports quickly, Metro needs to know what files exist without hitting the disk on every resolution.

    metro-file-map builds an in-memory map of the project:

    • Watches projectRoot + watchFolders
    • Applies blockList regexes (hide paths from resolution)
    • Supports symlinks (targets must still be visible to Metro)
    • Integrates with Watchman when available (fast, scalable watching)
    • Caches to fileMapCacheDirectory (default: OS temp dir)
    Why this matters for resolution

    The resolver’s fileSystemLookup(path) consults this map — not fs.existsSync per lookup. That makes resolution O(1) map lookup instead of O(disk I/O).

    Rendering diagram…
    Haste (legacy opt-in)

    Metro historically supported Haste: importing modules by global name (import Foo from 'Foo') without relative paths. This requires hasteImplModulePath and is rare in modern projects. The file map still exposes resolveHasteModule / resolveHastePackage for this mode.


    10. Resolution — how imports become file paths

    Resolution is the most common source of “Unable to resolve module” errors. Understanding the algorithm beats guessing.

    Resolution context

    Every resolve call carries a context including:

    FieldMeaning
    originModulePathAbsolute path of the file containing the import
    platformios, android, web, etc.
    devDevelopment vs production
    sourceExtsExtensions to try, in order
    assetExtsExtensions treated as assets
    mainFieldspackage.json entry fields to read
    nodeModulesPathsExtra lookup directories
    extraNodeModulesPackage name → forced directory
    resolveRequestCustom resolver hook
    fileSystemLookupMap-based existence check
    The algorithm (RESOLVE) — simplified but complete

    For import moduleName from originModulePath:

    Plain text
    RESOLVE_MODULE (a path without extension)
    Plain text
    RESOLVE_FILE (platform + extension cascade)

    For platform android and sourceExts = ['js', 'jsx', 'ts', 'tsx'], Metro tries in order:

    Plain text

    First match wins. This is how Button.ios.tsx and Button.android.tsx work.

    RESOLVE_PACKAGE (npm packages)

    When resolving node_modules/react-native:

    1. If package.json has "exports" and exports are enabled → resolve via conditional exports (import vs require, react-native, browser, default).
    2. Else → RESOLVE_MODULE to main / react-native field path.
    Package exports (modern npm)

    Since Metro 0.82+, unstable_enablePackageExports defaults to true.

    BehaviorDetail
    Honors exports mapSubpaths like pkg/dist/internal may be blocked
    Conditional exportsAsserts require or import based on dependency type
    Platform conditionsWeb adds browser condition; RN adds react-native
    No platform extensions inside exportsIf exports matches, .ios.ts variants are NOT tried (Node compatibility)
    FallbackIf no exports match, warns and falls back to legacy resolution
    Browser field spec

    Packages can remap or stub modules for non-Node environments:

    JSON

    false → Metro’s empty module (a built-in no-op shim).

    Resolution types returned
    TypeMeaning
    { type: 'sourceFile', filePath }Transform as JS module
    { type: 'assetFiles', filePaths: [...] }Register as asset(s)
    { type: 'empty' }Use empty module shim
    Dynamic requires
    JavaScript

    Metro cannot statically resolve this. Config dynamicDepsInPackages:

    • 'throwAtRuntime' (default) — bundle succeeds; throws when line runs
    • 'reject' — fail the build

    11. Transformation — how source becomes runnable JS

    Default pipeline
    Rendering diagram…
    What transformation produces

    For each module, the transformer returns:

    1. Code — JavaScript the serializer will wrap
    2. Source map — mapping output lines → original source
    3. Dependency descriptors — static imports found in this file (names + locs)
    4. Metadata — inlineRequires hints, asset references, etc.
    Babel’s role

    Metro does not run tsc for emit. TypeScript types are erased by Babel. Type-checking is a separate step (tsc --noEmit) you run in CI or your editor.

    Common Babel plugins in RN projects:

    • @babel/preset-typescript — strip types
    • @babel/preset-react — JSX
    • react-native-reanimated/plugin — must be last in plugin list
    • babel-plugin-module-resolver — if used for aliases (Expo often uses tsconfig paths instead)
    Worker parallelism
    SettingEffect
    maxWorkers~os.availableParallelism() / 2 by default
    maxWorkers: 1Workers run in main process (debugging)
    stickyWorkers: trueSame file → same worker (faster warm transforms)

    Two worker pools: transform pool and file-map pool, each sized by maxWorkers.

    getTransformOptions

    A callback Metro invokes per bundle request to set:

    OptionPurpose
    inlineRequiresLazy require() inside modules (prod startup optimization)
    nonInlinedRequiresPackages to exclude from inlining
    preloadedModulesModules to eagerly execute in RAM bundles
    ramGroupsGroup modules for RAM bundle segmentation

    React Native’s release builds often enable inline requires to improve startup by deferring module initialization until first use.

    Custom transformers

    transformer.babelTransformerPath or top-level transformerPath can point to a custom module implementing Metro’s transformer interface — used for MDX, CSS, SVG-as-components, etc.


    12. Serialization — how the graph becomes a bundle

    Serialization assigns numeric module IDs and emits the module factory table the runtime executes.

    Module wrapping

    Each module becomes a call to __d (define):

    JavaScript
    PieceRole
    __dRegister module factory
    moduleIdUnique integer in this bundle
    dependencyMapMaps local require indices → module IDs
    Factory functionExecutes once when module is first required
    Dependency map

    If App.tsx imports react and ./Button:

    JavaScript

    The runtime resolves r(d[0]) → require(0) → executes module 0’s factory.

    Serializer responsibilities
    1. Topological ordering — dependencies before dependents (with cycle handling)
    2. Module ID assignment — via createModuleIdFactory (stable IDs matter for RAM bundles and HMR)
    3. Optional filtering — processModuleFilter to exclude modules
    4. Run modules — runModule / runBeforeMainModule for side-effect ordering
    5. Minification — terser in production
    6. Source map composition — merge per-file maps into bundle map
    Multiple outputs
    OutputWhen
    Single JS stringDefault dev + prod
    RAM bundle (binary)iOS release optimizations
    Multiple chunksWeb + dynamic import()
    Source map fileProduction debugging

    13. The Metro runtime — how bundles actually execute

    The bundle is not native ESM. It is an IIFE that installs a custom module system from metro-runtime.

    Boot sequence (conceptual)
    Rendering diagram…
    Key runtime functions
    SymbolPurpose
    __dDefine a module
    __r / requireLoad module by ID (with cache)
    __cClear module cache (HMR)
    globalRN’s global object
    Module cache

    Once require(5) runs module 5’s factory, the exports object is cached. Subsequent require(5) returns cached exports. HMR uses __c to invalidate specific IDs before re-executing updated factories.

    __DEV__ global

    Metro defines __DEV__ as true in development bundles and false in production. Dead code wrapped in if (__DEV__) can be stripped in prod minification.

    asyncRequire and dynamic import

    import('./chunk') is transformed to use asyncRequire from metro-runtime, which loads additional bundles at runtime (platform-specific behavior).


    14. Bundle formats: plain, indexed RAM, file RAM

    Metro supports three bundle formats:

    Plain bundle (default)
    • All modules concatenated into one JS file
    • Each module wrapped in __d(...)
    • Used by: dev server, web, most Android/iOS dev builds
    • Request: index.bundle?platform=ios&dev=true
    Indexed RAM bundle (iOS historical optimization)

    Binary format with:

    SectionContent
    Magic 0xFB0BD1E5Format identifier
    Offset table(offset, length) pairs per module
    Startup codeEagerly loaded preamble
    Module bodiesNull-terminated module code

    Why: Load any module in O(1) time via offset table — important when entire bundle is in memory.

    File RAM bundle (Android historical optimization)
    • Each module → separate file js-modules/<id>.js
    • UNBUNDLE marker file at root with magic number
    • Why: Android APKs are zip archives; random access to small zip entries beats parsing one giant binary
    Rendering diagram…

    Modern Hermes + improved startup have reduced RAM bundle dominance, but the formats remain in Metro’s serializer.


    15. The dev server and HTTP API

    Metro’s dev server (default port 8081) is an HTTP server built into the metro package.

    Common endpoints
    RequestPurpose
    GET /index.bundle?platform=ios&dev=true&minify=falseFull JS bundle
    GET /index.map?platform=ios&dev=trueSource map
    GET /assets/...Serve image/font assets
    GET /symbolicatePOST stack frames → original locations
    WebSocket /hotHMR events
    Bundle URL parameters
    ParamEffect
    platformios, android, web — drives resolution
    devtrue / false
    minifyEnable minification
    modulesOnlyReturn only specific modules (HMR)
    runModuleWhether to execute entry after define
    shallowGraph depth control (advanced)
    Middleware

    server.enhanceMiddleware lets you attach custom HTTP middleware (proxy, auth, custom endpoints).

    Multi-client

    One Metro server can serve simultaneous iOS simulator, Android emulator, and web — each requests a different platform graph. Graphs overlap heavily but are not identical (platform files differ).


    16. HMR, live reload, and Fast Refresh

    Three related but distinct mechanisms:

    MechanismLayerWhat happens
    Live reloadDev serverFull page/app reload on any change
    HMR (Hot Module Replacement)MetroSwap changed modules in running bundle
    Fast RefreshReactHMR + preserve React component state
    HMR flow
    Rendering diagram…
    Fast Refresh rules (React layer)

    Fast Refresh is enabled by a Babel plugin (react-refresh/babel) coordinated with Metro HMR.

    Edit targetResult
    File exporting only React componentsHot swap, state preserved
    File with non-component exportsRe-run module + all importers
    File imported by non-React codeFull reload fallback
    Syntax errorRedbox; fix + save recovers without manual reload
    Runtime error in componentRedbox; fix remounts component

    Hook behavior during Fast Refresh:

    • useState / useRef — preserve values
    • useEffect / useMemo / useCallback — always re-run during refresh (even empty deps)

    Force remount: add // @refresh reset in a file.

    What Metro HMR does NOT fix
    • Native code changes (Swift, Kotlin, C++) → rebuild native app
    • metro.config.js changes → restart Metro
    • New native module install → rebuild dev client
    • Changes outside the graph (e.g. app.json native config) → full reload

    17. Caching — every layer explained

    Metro has multiple independent caches. Confusing them causes “I cleared cache but it’s still broken.”

    Rendering diagram…
    Transform cache
    PropertyDetail
    KeyHash of file content + relevant config + transformer version
    ValueTransformed code + source map
    StorescacheStores — default FileStore in temp dir
    OverridecacheVersion string appended to all keys
    Reset--reset-cache, resetCache: true, delete metro-* in tmpdir
    File map cache
    PropertyDetail
    LocationfileMapCacheDirectory (default OS tmp)
    InvalidateswatchFolders change, blockList change, structural FS changes
    ResetresetCache: true
    Remote cache (Meta-scale teams)

    HttpStore / HttpGetStore in cacheStores:

    1. CI runs metro build → populates remote cache
    2. Dev machines read transforms from HTTP cache on miss
    3. Local FileStore listed first for speed
    What invalidates what
    ChangeTransform cacheFile mapIn-memory graph
    Edit one .tsx fileThat file onlyNoDirty subgraph
    npm installPotentially manyYesFull rebuild
    metro.config.jsAllMaybeFull rebuild
    babel.config.jsAllNoFull rebuild
    package.json exports changeAffected packagesYesAffected subgraph

    18. File watching: Watchman and rebuild triggers

    Watchman

    Watchman is Meta’s file-watching service. Metro prefers it over Node’s fs.watch because it scales to huge trees.

    SettingEffect
    watcher.useWatchman: trueDefault when Watchman installed
    watcher.useWatchman: falseFall back to Node crawler (slower)

    Troubleshooting: watchman watch-del-all clears stale watches.

    Rebuild trigger chain
    Plain text
    watchFolders vs watching

    Despite the name, watchFolders defines visibility, not just watching. Files outside projectRoot + watchFolders cannot be resolved even in CI offline builds.


    19. Platform-specific resolution

    React Native’s platform system is implemented inside Metro’s resolver, not in the RN runtime.

    Extension priority (example: platform=ios)

    For file utils.ts:

    Plain text

    For platform=web:

    Plain text
    Separate graphs
    Rendering diagram…

    Same import statement. Three different resolved files possible.

    platforms config

    Default: ['ios', 'android', 'windows', 'web']. Add custom platforms if needed.


    20. The asset pipeline

    Assets (images, fonts, video) are not executed as JS. They flow through a parallel registry.

    Importing an asset
    JavaScript
    Resolution
    1. icon.png matches assetExts
    2. RESOLVE_ASSET collects all variants: icon.png, icon@2x.png, icon@3x.png
    3. Serializer registers asset with metadata (dimensions, scales)
    Runtime
    ModeBehavior
    DevAsset served from http://localhost:8081/assets/...
    ProdAssets copied into app package; IDs map to packaged paths
    assetResolutions

    Default scale suffixes: @2x, @3x, etc. Customizable in config.


    21. Source maps

    Metro composes source maps across the transform chain so debuggers show original TypeScript/JSX.

    Chain
    Plain text
    Uses
    ConsumerPurpose
    Chrome / Safari devtoolsBreakpoints in original source
    React Native redboxError overlay with TS lines
    metro-symbolicateConvert production stack traces
    Production source maps

    Often external (index.map) or hidden (not referenced in bundle) to reduce download size while keeping maps for crash reporting.


    22. Development vs production builds

    DimensionDevelopment (dev=true)Production (dev=false)
    __DEV__truefalse
    MinificationOffOn (terser)
    Inline requiresUsually offOften on (RN release)
    Source mapsCheap / inlineExternal / optimized
    Error messagesVerboseStripped
    Dead code in if (__DEV__)KeptRemoved
    Bundle sizeLargeSmaller
    Transform cacheHeavy useCI + release pipeline
    Fast RefreshOnN/A
    NODE_ENV vs __DEV__

    Metro sets __DEV__ from the dev bundle param. process.env.NODE_ENV may also be inlined by Babel — they should align but are set by different mechanisms.


    23. Code splitting and dynamic import

    Web

    import('./heavy') typically produces:

    • Main bundle (entry)
    • Separate async chunk(s)
    • Runtime loader via asyncRequire / web chunk loader
    Native

    Native code splitting is less mature than web. Dynamic import is transformed but behavior depends on RN version and configuration. Historically, RAM bundles + inline requires were the native startup strategy rather than web-style chunks.

    __loadBundleAsync

    Modern proposal for explicit lazy bundle loading on native — replaces older asyncRequireModulePath patterns.


    24. Hermes and the post-Metro pipeline

    Metro’s output is JavaScript text. Hermes is a separate step in release builds.

    Rendering diagram…
    EngineMetro relationship
    HermesConsumes Metro JS bundle → bytecode
    JSCExecutes Metro JS bundle directly
    V8 (web)Browser executes Metro web bundle

    Metro does not know about Hermes. The native build pipeline invokes hermesc after bundling.


    25. Configuration — the full surface

    Config file priority (highest first):

    1. metro.config.js / .cjs / .mjs
    2. metro.config.ts
    3. metro.config.json
    4. .config/metro.*
    5. package.json → "metro" field
    Top-level options
    OptionPurpose
    projectRootRoot of the Metro project
    watchFoldersAdditional visible directories
    cacheStoresTransform cache backends
    cacheVersionForce cache invalidation
    resetCacheClear caches on startup
    maxWorkersParallelism
    stickyWorkersPin files to workers
    reporterProgress / event reporting
    transformerPathCustom transformer module
    resolver
    OptionPurpose
    sourceExtsTry extensions in order
    assetExtsNon-JS file extensions
    resolverMainFields['react-native', 'browser', 'main'] in RN
    blockListExclude paths from graph
    extraNodeModulesForce package locations
    nodeModulesPathsExtra node_modules roots
    resolveRequestCustom resolution hook
    disableHierarchicalLookupSkip upward node_modules walk
    unstable_enablePackageExportsHonor exports field
    unstable_conditionNamesGlobal export conditions
    unstable_conditionsByPlatformPer-platform export conditions
    platformsSupported platform strings
    transformer
    OptionPurpose
    babelTransformerPathCustom Babel integration
    getTransformOptionsPer-bundle inlineRequires, RAM groups
    dynamicDepsInPackagesthrowAtRuntime vs reject
    unstable_allowRequireContextWebpack-style require.context
    serializer
    OptionPurpose
    createModuleIdFactoryStable / deterministic module IDs
    processModuleFilterExclude modules from output
    customSerializerReplace entire serialization
    getRunModuleStatementCustomize entry execution
    isThirdPartyModuleMark node_modules for optimizations
    server
    OptionPurpose
    portDefault 8081
    enhanceMiddlewareCustom HTTP middleware
    watcher
    OptionPurpose
    watchman.deferStatesWatchman perf tuning
    healthCheck.enabledWatchman health checks

    26. Monorepos and workspaces

    Monorepos break Metro if configured naively.

    Common problems
    ProblemCauseFix
    Cannot resolve workspace packagePackage outside projectRootAdd to watchFolders
    Duplicate ReactTwo copies in different node_modulesblockList, hoisting, extraNodeModules
    Symlinked packages not foundSymlink target outside watchInclude real path in watchFolders
    Slow startupWatching entire repoNarrow watchFolders to needed packages
    Recommended pattern
    JavaScript
    enableGlobalPackages

    When true, any package.json with a name field under watchFolders can be imported by package name (workspace-style).


    27. Custom resolvers and transformers

    Custom resolver
    JavaScript

    Rules:

    • Return absolute real paths
    • Throw or delegate — never return undefined
    • Chain to context.resolveRequest for fallback
    Custom serializer

    serializer.customSerializer receives the full graph and can emit any output format — used by advanced tooling, not typical apps.


    28. Tree shaking, dead code, and Metro’s limits

    What Metro does
    OptimizationHow
    Minificationterser removes dead code in production
    __DEV__ strippingif (__DEV__) blocks removed when dev=false
    Inline requiresDefer module init until needed
    processModuleFilterManual exclusion
    What Metro does NOT do (vs Webpack/Rollup)
    LimitationImpact
    No full ESM tree shakingimport { a } from 'barrel' may still pull entire barrel’s re-exports into graph
    CommonJS interoprequire() dynamic nature limits static analysis
    Side-effect-free markingNo sideEffects: false equivalent as aggressive as Rollup
    Practical advice
    • Import directly from leaf modules, not giant index re-exports
    • Use dynamic import for heavy optional features
    • Enable inline requires for release native builds
    • Measure with bundle visualizers (Expo Atlas, react-native-bundle-visualizer)

    29. Debugging Metro failures

    Error anatomy

    A typical resolution error:

    Plain text

    Read four things:

    1. Module name (X)
    2. Importer (Y)
    3. Platform (from URL or CLI)
    4. Parent chain (who imported who)
    Common failures
    SymptomLikely cause
    Unable to resolve moduleMissing dep, wrong alias, exports blocked
    Duplicate ReactTwo React copies in graph
    Stale code after editCache corruption
    Works on iOS, fails web.native.ts or Node API without web shim
    OOM during bundleGraph too large — dynamic import
    Slow rebuildsWatchman broken, huge watchFolders
    Reset recipe (official troubleshooting order)
    Bash
    Verbose logging
    Bash
    Symbolication

    Production crashes show minified names. POST stack frames to Metro’s /symbolicate endpoint (or use RN tooling) with the release source map.


    30. Metro vs Webpack vs Vite vs Rollup

    DimensionMetroWebpackViteRollup
    Primary targetReact NativeWeb (universal)WebLibraries / web
    Dev modelIncremental graph + HTTPBundle or lazy compileNative ESM + esbuild prebundleN/A (build tool)
    Module system in outputCustom require IDsVariousESM in devESM
    HMRYes + Fast RefreshYesYesNo
    Code splittingWeb-strong; native differsMatureMatureMature (ESM)
    Tree shakingLimitedStrong (ESM)StrongStrongest
    Config complexityModerateHighLower (web)Moderate
    IncrementalCore designAdded (persistent cache)Dev server native ESMPer-build

    Why not use Webpack for RN? Historical integration, custom module runtime, platform resolution, and dev incremental performance on mobile-scale graphs.


    31. End-to-end walkthrough: one edit, one rebuild

    Concrete scenario: you change Button.tsx in a running dev session.

    Plain text
    Rendering diagram…

    32. Glossary

    TermDefinition
    AssetNon-JS file (image, font) registered by Metro, executed as numeric ID
    Async dependencyTarget of dynamic import() — may be separate chunk
    blockListRegex excluding paths from Metro’s file map
    BundlePacked JS output containing module factories + runtime
    ChunkOne of multiple output bundles (code splitting)
    Conditional exportspackage.json exports branches on import/require/platform
    DeltaSet of graph changes since last bundle (HMR/incremental)
    DeltaBundlerMetro subsystem maintaining incremental dependency graph
    Dependency graphModules (nodes) + imports (edges) reachable from entry
    Empty moduleNo-op shim when browser field maps to false
    Entry pointWhere graph traversal begins (e.g. index.js)
    Fast RefreshReact-aware HMR preserving component state
    File mapIn-memory index of project files (metro-file-map)
    HasteLegacy global module name imports (opt-in)
    Hermes bytecode (HBC)Compiled output of Hermes, post-Metro
    HMRHot Module Replacement — swap modules without full reload
    Inline requiresTransform require to lazy calls inside module body
    Module IDInteger identifying a module in the bundle
    metro-runtime__d, __r, __c — module system in the bundle
    Platform resolutionChoosing .ios.ts / .web.ts variants
    RAM bundleBinary or multi-file bundle format for O(1) native module load
    ResolverImport string → absolute path algorithm
    SerializerGraph → bundle bytes
    Source mapMaps bundled JS back to original source
    Transform cacheDisk/memory store of per-file Babel output
    TransformerPer-file compiler (typically Babel)
    watchFoldersDirectories Metro can see beyond projectRoot
    WatchmanFile watching service used by Metro

    33. Further reading

    • Metro concepts: https://metrobundler.dev/docs/concepts
    • Module resolution (full algorithm): https://metrobundler.dev/docs/resolution
    • Configuration reference: https://metrobundler.dev/docs/configuration
    • Caching: https://metrobundler.dev/docs/caching
    • Bundle formats: https://metrobundler.dev/docs/bundling
    • Metro GitHub: https://github.com/facebook/metro
    • React Native Fast Refresh: https://reactnative.dev/docs/fast-refresh
    • React Native images (assets): https://reactnative.dev/docs/images
    • Hermes: https://hermesengine.dev/
    • Customizing Metro (Expo): https://docs.expo.dev/guides/customizing-metro/

    Quick reference card

    Plain text

    General-purpose Metro reference. File: ~/Downloads/metro-bundler-deep-dive.md