skip to content

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%

answer

  1. content hash plus relative path
  2. dev, platform, minify, inlineRequires in the key
  3. Metro version, transformer code, cacheVersion
  4. bare RN: babel.config.js edits not keyed
  5. env vars read by plugins invisible

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.

solid answer

~50 s

Each transformed file is cached under a key with three layers. The **global** part hashes Metro's version, `cacheVersion` (default `'1.0'`), the transform worker's source, the transformer config apart from its module paths, and a key contributed by the Babel transformer. The **per-bundle** part adds options such as `dev`, `platform`, `minify`, `inlineRequires` and `experimentalImportSupport`. The **per-file** part adds the project-relative path and a SHA-1 of the content. What it misses: anything a Babel plugin reads at transform time that is not a config file, such as `process.env` values; and in a bare React Native 0.87 app, edits to `babel.config.js`, because `@react-native/metro-babel-transformer` contributes only the preset version and its own source. Metro's default transformer and Expo's transformer do hash the Babel config files. Fix a gap by resetting once, or permanently by folding the input into `cacheVersion`.

code

javascript · 8 lines
javascript
// metro.config.js (React Native 0.87)
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

module.exports = mergeConfig(getDefaultConfig(__dirname), {
  // A Babel plugin inlines process.env.APP_ENV, which Metro cannot see.
  // Folding it into cacheVersion gives each environment its own cache entries.
  cacheVersion: `app-env-${process.env.APP_ENV ?? 'dev'}`,
});

go deeper

for a junior

Recall that Metro caches each compiled file and that editing the file itself always produces fresh output.

for a middle

List the key's parts, content hash, relative path, transform options, Metro version, cacheVersion and transformer config, and name an input it cannot see.

for a senior

Diagnose stale output by asking which input changed without reaching the key, and close the gap with cacheVersion or a framework mechanism instead of routine resets.

for a principal

Define which build inputs are allowed to influence transforms, so the cache stays trustworthy locally, on CI and in any shared remote cache.

## Why the key matters Metro's transform cache is only correct if its key changes whenever the output of a transform would change. If an input that affects the output is left out of the key, Metro returns yesterday's result for today's input: **stale output**, with no error. Knowing what is in the key tells you which changes are safe and which need a reset. ## The three layers of the key **1. Global, computed once per Metro start** - the text `metro-cache` and **Metro's version**; - **`cacheVersion`**, a free-form string from the config, `'1.0'` by default; - a hash of the transform worker module (`transformerPath`); - the transform worker's own key: its source files, the source of the Babel transformer and minifier it loads, a stable hash of the transformer config with the module paths removed, and whatever the **Babel transformer's `getCacheKey`** returns. **2. Per bundle request** - `dev`, `minify`, `platform`, `type`, `inlinePlatform`, `inlineRequires`, `nonInlinedRequires`, `experimentalImportSupport`, `unstable_transformProfile` and `customTransformOptions`. So an iOS development bundle and an Android release bundle never share entries. **3. Per file** - the path **relative to `projectRoot`**, which lets the key be portable across machines; - a **SHA-1 of the file's contents**. Editing a source file therefore always produces a new key, and a file moved to another folder gets its own entry. ## What each Babel transformer adds | Babel transformer | Contribution to the key | |---|---| | `metro-babel-transformer` (Metro's default) | hashes of the Babel config files that apply to the project | | `@react-native/metro-babel-transformer` (React Native CLI) | the `@react-native/babel-preset` version and the transformer's own source | | Expo's Babel transformer | hashes of the project's Babel config files and what they load | This is the most practical row in the table. In a bare React Native 0.87 app, adding a plugin to `babel.config.js` does **not** change the key, so already-cached files keep their old output until you run `npx react-native start --reset-cache`. In an Expo project, the Babel config is part of the key, and Expo's config also puts the installed Reanimated and Worklets versions into the transformer config so their Babel plugins invalidate the cache on upgrade. ## What no key can see - **Environment variables read by a Babel plugin.** A plugin that inlines `process.env.API_URL` produces different output when the variable changes, but neither the file nor the config changed. - **Other files a plugin reads**, such as a JSON file of feature flags consumed at transform time. - **Plugin code upgraded in `node_modules`** in setups where the plugin's version is not otherwise part of the key. ## A worked example A bare React Native app adds a Babel alias plugin so that `@/components/Button` maps to `src/components/Button`. The developer edits `babel.config.js`, reloads, and files that were already cached still contain the old, unaliased import, so Metro reports it cannot resolve `@/components/Button` from some files but not others. Files edited since then work, because their content hash changed. The pattern, some files fine and others stale after a config change, is the signature of an input missing from the key. One `npx react-native start --reset-cache` makes every file consistent again. ## Closing a gap on purpose 1. **Reset once** after the change: the quick fix. 2. **Put the input into `cacheVersion`.** Because `cacheVersion` is part of the global key, deriving it from the input makes the cache follow it automatically: ```js module.exports = mergeConfig(getDefaultConfig(__dirname), { cacheVersion: `app-env-${process.env.APP_ENV ?? 'dev'}`, }); ``` 3. **Prefer the framework's mechanism** where one exists, so the value is not inlined by an ad-hoc plugin at all. ## Portability as a side effect Because the per-file part uses project-relative paths and content hashes, and the global part excludes absolute module paths, the same key is computed on different machines for the same inputs. That is what makes a shared remote cache possible. ## What interviewers probe A middle answer lists content hash, path, transform options and the global parts, and then names at least one input the key cannot see. The strongest answers know the difference between the bare React Native and Expo Babel transformers, and use `cacheVersion` rather than habitual resets.

  • Why does Metro include the project-relative path in the key when it already hashes the content?
    Transformers receive the file path and may behave differently by extension or location, so two files with identical content can compile differently. Using the path relative to `projectRoot`, not the absolute path, keeps keys identical across machines.
  • In a bare React Native app, you add a plugin to babel.config.js and old output persists. Why, and what are the fixes?
    `@react-native/metro-babel-transformer` keys its output on the preset version and its own source, not on your Babel config file, so cached files keep their old output. Run `npx react-native start --reset-cache` once; if the config changes often, bump `cacheVersion` with it.

saying these in an interview costs you the question

  • Metro's cache key is just the file name
  • Any change to babel.config.js always invalidates the cache
  • Environment variables inlined by Babel are part of the key
  • iOS and Android bundles share the same cached output
  • cacheVersion must never be changed by a project