skip to content

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%

answer

  1. default: FileStore in os.tmpdir()/metro-cache
  2. persist the FileStore root between CI runs
  3. HttpStore writes, HttpGetStore only reads
  4. stores tried in order, misses written back
  5. keys portable: relative paths, content hashes

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.

solid answer

~50 s

By default `cacheStores` holds one `FileStore` under `os.tmpdir()/metro-cache`, which a fresh CI runner never has, so every release bundle transforms every file. For CI, set `new FileStore({root: ...})` to a folder inside the workspace and let the CI system save and restore it between runs, keyed on the lockfile. For a team, Metro supports a remote cache: a CI job runs Metro and writes to an `HttpStore` pointing at your storage backend, and developer configs add a read-only `HttpGetStore` after the local `FileStore`. Metro reads stores in order and writes a result to every store that missed, so the local store fills from the remote one. It works because keys contain relative paths, content hashes and config hashes, not absolute paths. Keep all machines on the same Metro, transformer and Babel versions, or keys simply never match. `AutoCleanFileStore` is deprecated.

code

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

const isCI = process.env.CI === 'true';
const remote = process.env.METRO_REMOTE_CACHE_URL; // team-defined variable

module.exports = mergeConfig(getDefaultConfig(__dirname), {
  cacheStores: ({FileStore, HttpStore, HttpGetStore}) => {
    const stores = [
      new FileStore({root: path.join(__dirname, '.metro-cache')}),
    ];
    if (remote) {
      // Only CI writes; developers read.
      stores.push(
        isCI
          ? new HttpStore({endpoint: remote, timeout: 5000})
          : new HttpGetStore({endpoint: remote, timeout: 5000}),
      );
    }
    return stores;
  },
});

go deeper

for a junior

Recall that Metro stores compiled files in a cache under the system temp folder, which a fresh CI machine does not have.

for a middle

Explain cacheStores order and write-back, and configure a FileStore in a folder the CI system can persist.

for a senior

Design the HttpStore and HttpGetStore split, keep versions aligned so keys match, and treat write access to the remote cache as a security boundary.

for a principal

Weigh a shared transform cache against its operating cost and trust model, and decide whether CI caching alone meets the build-time budget.

## The problem Metro's transform cache makes warm starts fast, but CI rarely starts warm. The default configuration has one store: ```js cacheStores: [new FileStore({root: path.join(os.tmpdir(), 'metro-cache')})] ``` A fresh CI runner has an empty temporary directory, so every release build transforms every file in the graph, including all of `node_modules`. On a large app that is minutes of CPU per build, repeated for each platform. **`cacheStores`** is the option that changes where results live and who can reuse them. ## How cacheStores works - It is a list of **cache stores** (or a function that receives Metro's cache classes and returns the list). - For each file, Metro computes a **machine-independent key** and asks each store in order. - The first hit wins. After obtaining the result, Metro writes it to **every store that returned a miss**, so an earlier, faster store fills up from a later one. - A store implements `get(key)`, `set(key, value)` and `clear()`; a read-only store simply ignores `set`. Built-in stores: | Store | Behaviour | |---|---| | `FileStore({root})` | files under a directory; the default | | `AutoCleanFileStore` | deprecated `FileStore` variant that deleted old entries | | `HttpStore({endpoint, timeout, …})` | remote read (`GET`) and write (`PUT`) of compressed entries | | `HttpGetStore` | read-only `HttpStore` | ## Option 1: persist a FileStore on CI 1. Point the store at a folder inside the job's workspace. 2. Tell the CI system to save and restore that folder between runs, keyed on the lockfile and Metro config so a dependency change starts a fresh cache. 3. Apply the same idea to the file map with **`fileMapCacheDirectory`** if crawling is a noticeable part of the time. This needs no infrastructure beyond the CI system's own cache feature, and it helps every build after the first on a given branch. ## Option 2: a shared remote cache Metro's docs describe the team setup: 1. A storage backend the team controls, reached over HTTP or HTTPS. 2. A **CI job** that regularly bundles the app with an `HttpStore` in its config, so it populates the remote cache. 3. **Developer machines** configured with a local `FileStore` first and a read-only **`HttpGetStore`** second, so they read remote results but never write untrusted entries. Local stays first because it is fastest; a remote hit is copied into the local store automatically. ## Why sharing is safe, and when it silently fails The key is designed to be portable: file paths are **relative to `projectRoot`**, content is identified by a **SHA-1 hash**, and the transformer config is hashed without its absolute module paths. Two machines with the same inputs compute the same key. That same design explains the silent failure mode: the key includes Metro's version, the transformer's source and the Babel transformer's contribution. If developers run different versions of Metro, the React Native preset or Babel plugins than CI, keys never match. Nothing breaks; the cache just never hits. Pin dependencies with a lockfile and keep CI and developers on the same installs. ## Operational cautions - **Treat a writable remote cache as a trusted build input.** Whoever can write entries can change the code other machines bundle, so only CI should write; developers get read-only access. - **Set a timeout** on remote stores (`HttpStore`'s default is 5000 ms) so a slow backend degrades to a miss, not a hung build. - **Measure hit rates** before and after; if they stay near zero, look for version drift or an unstable input. - **Do not combine a shared cache with keys that miss inputs**, such as environment variables inlined by a Babel plugin; fold them into `cacheVersion` first, or the cache will share wrong output. ## What a senior answer shows It explains why CI starts cold, persists a `FileStore` as the first cheap step, describes the documented `HttpStore` and `HttpGetStore` split with write access limited to CI, and knows that portable keys make sharing work while version drift quietly defeats it.

  • The team set up a shared Metro cache, but developers see almost no remote hits. What would you check?
    Whether everyone runs the same Metro, React Native preset and Babel plugin versions as CI, since those are part of the global key. Then check for inputs that differ per machine, such as a `cacheVersion` derived from a local variable, and that the endpoint and timeout are reachable.
  • Why should developer machines use HttpGetStore rather than HttpStore?
    `HttpGetStore` is read-only. Letting every laptop write means any local modification, plugin experiment or compromised machine can publish transform output that others then bundle. Keeping writes on CI makes the remote cache as trustworthy as the CI pipeline.

saying these in an interview costs you the question

  • Metro's default cache survives between fresh CI runners
  • Cache keys contain absolute paths, so sharing across machines cannot work
  • Every developer should write to the shared remote cache
  • Metro reads the remote store first because it holds more entries
  • AutoCleanFileStore is the recommended store for new projects