Skip to main content
Metro is the JavaScript bundler for React Native. Learn how to configure Metro for optimal development and production builds.

Overview

Metro configuration is defined in metro.config.js at your project root. React Native provides sensible defaults through the @react-native/metro-config package.

Basic Configuration

Default Setup

Create metro.config.js:
metro.config.js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

const config = {};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);
This uses React Native’s default Metro configuration.

Configuration Options

Metro configuration is divided into several sections:

Resolver

Controls how Metro resolves modules.
metro.config.js
const config = {
  resolver: {
    // File extensions to resolve
    sourceExts: ['jsx', 'js', 'ts', 'tsx', 'json'],
    
    // Asset file extensions
    assetExts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'],
    
    // Platforms to support
    platforms: ['ios', 'android'],
    
    // Node modules to exclude
    blockList: [/node_modules\/.*\/node_modules\/react-native\/.*/],
    
    // Extra node modules to include
    extraNodeModules: {},
    
    // Unstable condition names
    unstable_conditionNames: ['react-native', 'browser', 'require'],
    
    // Resolver main fields (package.json)
    resolverMainFields: ['react-native', 'browser', 'main'],
  },
};

Transformer

Controls how files are transformed.
metro.config.js
const config = {
  transformer: {
    // Babel transformer path
    babelTransformerPath: require.resolve('@react-native/metro-babel-transformer'),
    
    // Asset registry path
    assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
    
    // Async require module path
    asyncRequireModulePath: require.resolve('metro-runtime/src/modules/asyncRequire'),
    
    // Allow optional dependencies
    allowOptionalDependencies: true,
    
    // Transform options
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
    
    // Minifier configuration
    minifierConfig: {
      compress: {
        drop_console: true,
      },
    },
  },
};

Serializer

Controls how modules are serialized into bundles.
metro.config.js
const config = {
  serializer: {
    // Get polyfills
    getPolyfills: () => require('@react-native/js-polyfills')(),
    
    // Modules to run before main module
    getModulesRunBeforeMainModule: () => [
      require.resolve('react-native/Libraries/Core/InitializeCore'),
    ],
    
    // Determine if module is third-party
    isThirdPartyModule: (module) => {
      return /node_modules/.test(module.path);
    },
    
    // Process modules before finalizing
    processModuleFilter: (module) => {
      return true;
    },
  },
};

Server

Controls Metro development server.
metro.config.js
const config = {
  server: {
    // Server port
    port: Number(process.env.RCT_METRO_PORT) || 8081,
    
    // Enable HTTPS
    https: false,
    
    // Custom SSL key/cert
    // key: fs.readFileSync('path/to/key.pem'),
    // cert: fs.readFileSync('path/to/cert.pem'),
  },
};

Watcher

Controls file watching behavior.
metro.config.js
const config = {
  watchFolders: [
    // Additional folders to watch
    path.resolve(__dirname, '../shared'),
  ],
  
  watcher: {
    // Watchman configuration
    watchman: {
      deferStates: ['hg.update'],
    },
    
    // Additional watch options
    additionalExts: ['scss', 'sass'],
  },
};

Common Configurations

Add Custom File Extensions

metro.config.js
const config = {
  resolver: {
    sourceExts: ['jsx', 'js', 'ts', 'tsx', 'json', 'mjs', 'cjs'],
  },
};

Support SVG

Using react-native-svg-transformer:
npm install react-native-svg-transformer
metro.config.js
const {getDefaultConfig} = require('@react-native/metro-config');

const config = {
  transformer: {
    babelTransformerPath: require.resolve('react-native-svg-transformer'),
  },
  resolver: {
    assetExts: getDefaultConfig(__dirname).resolver.assetExts.filter(
      ext => ext !== 'svg'
    ),
    sourceExts: [...getDefaultConfig(__dirname).resolver.sourceExts, 'svg'],
  },
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

Monorepo Setup

metro.config.js
const path = require('path');
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');

const config = {
  watchFolders: [workspaceRoot],
  resolver: {
    nodeModulesPaths: [
      path.resolve(projectRoot, 'node_modules'),
      path.resolve(workspaceRoot, 'node_modules'),
    ],
  },
};

module.exports = mergeConfig(getDefaultConfig(projectRoot), config);

Block Specific Modules

metro.config.js
const config = {
  resolver: {
    blockList: [
      // Block duplicate react-native instances
      /node_modules\/.*\/node_modules\/react-native\/.*/,
      
      // Block specific packages
      /.*\/__fixtures__\/.*/,
      /.*\/__tests__\/.*/,
    ],
  },
};

Custom Asset Handling

metro.config.js
const config = {
  resolver: {
    assetExts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ttf', 'otf', 'woff'],
  },
  transformer: {
    assetPlugins: ['custom-asset-plugin'],
  },
};

Environment-Specific Config

metro.config.js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

const isDev = process.env.NODE_ENV !== 'production';

const config = {
  transformer: {
    minifierConfig: {
      compress: {
        drop_console: !isDev,
        drop_debugger: !isDev,
      },
    },
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: !isDev,
      },
    }),
  },
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

Performance Optimization

Enable Inline Requires

metro.config.js
const config = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        inlineRequires: true,
      },
    }),
  },
};
This defers module loading until actually needed, reducing initial bundle size.

Cache Configuration

metro.config.js
const config = {
  cacheStores: [
    new (require('metro-cache'))({ root: '/tmp/metro-cache' }),
  ],
  cacheVersion: '1.0',
};

Limit Worker Threads

metro.config.js
const config = {
  maxWorkers: 2,
};
Useful for limiting CPU usage on lower-end machines.

Tree Shaking

metro.config.js
const config = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: true,
        inlineRequires: true,
      },
    }),
  },
};

Production Optimizations

Remove Console Logs

metro.config.js
const config = {
  transformer: {
    minifierConfig: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
};

Aggressive Minification

metro.config.js
const config = {
  transformer: {
    minifierPath: 'metro-minify-terser',
    minifierConfig: {
      compress: {
        drop_console: true,
        reduce_funcs: true,
        collapse_vars: true,
      },
      mangle: {
        toplevel: true,
      },
      output: {
        comments: false,
        ascii_only: true,
      },
    },
  },
};

Advanced Use Cases

Custom Transformer

custom-transformer.js
const upstreamTransformer = require('@react-native/metro-babel-transformer');

module.exports.transform = async ({src, filename, options}) => {
  // Custom transformation logic
  if (filename.endsWith('.custom')) {
    // Transform custom files
    return {
      ast: null,
      code: transformedCode,
    };
  }
  
  // Use default transformer for other files
  return upstreamTransformer.transform({src, filename, options});
};
metro.config.js
const config = {
  transformer: {
    babelTransformerPath: require.resolve('./custom-transformer'),
  },
};

Platform-Specific Extensions

metro.config.js
const config = {
  resolver: {
    sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json'],
    // Metro will look for Button.ios.js, Button.android.js, Button.native.js, Button.js
  },
};
File resolution order:
  1. Component.ios.js (on iOS)
  2. Component.android.js (on Android)
  3. Component.native.js
  4. Component.js

Custom Resolver

metro.config.js
const config = {
  resolver: {
    resolveRequest: (context, moduleName, platform) => {
      // Custom resolution logic
      if (moduleName.startsWith('custom:')) {
        return {
          filePath: '/path/to/custom/module.js',
          type: 'sourceFile',
        };
      }
      
      // Use default resolution
      return context.resolveRequest(context, moduleName, platform);
    },
  },
};

Debugging Metro Config

Verbose Logging

REACT_NATIVE_CLI_LOGLEVEL=verbose npx react-native start

Inspect Config

Create inspect-config.js:
inspect-config.js
const config = require('./metro.config.js');
console.log(JSON.stringify(config, null, 2));
Run:
node inspect-config.js

Test Config Changes

# Clear cache when testing config changes
npx react-native start --reset-cache

Integration with Tools

Storybook

metro.config.js
const config = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  resolver: {
    resolverMainFields: ['sbmodern', 'react-native', 'browser', 'main'],
  },
};

Web Support (React Native Web)

metro.config.js
const config = {
  resolver: {
    platforms: ['ios', 'android', 'web'],
    resolverMainFields: ['react-native', 'browser', 'main'],
  },
};

Troubleshooting

Module Not Found

Solution: Check resolver configuration:
const config = {
  resolver: {
    extraNodeModules: {
      'module-name': path.resolve(__dirname, 'path/to/module'),
    },
  },
};

Cache Issues

Solution:
npx react-native start --reset-cache
rm -rf /tmp/metro-*

Slow Bundling

Solution:
  1. Limit workers:
const config = {
  maxWorkers: 2,
};
  1. Use persistent cache
  2. Exclude unnecessary files from watching

Duplicate Module

Error: Requiring module 'X', which was already loaded Solution:
const config = {
  resolver: {
    blockList: [
      /node_modules\/.*\/node_modules\/react-native\/.*/,
    ],
  },
};

React Native Defaults

The default Metro configuration from @react-native/metro-config includes:
{
  resolver: {
    resolverMainFields: ['react-native', 'browser', 'main'],
    platforms: ['android', 'ios'],
    unstable_conditionNames: ['react-native'],
  },
  serializer: {
    getModulesRunBeforeMainModule: () => [
      require.resolve('react-native/Libraries/Core/InitializeCore'),
    ],
    getPolyfills: () => require('@react-native/js-polyfills')(),
  },
  server: {
    port: Number(process.env.RCT_METRO_PORT) || 8081,
  },
  transformer: {
    allowOptionalDependencies: true,
    assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
    asyncRequireModulePath: require.resolve('metro-runtime/src/modules/asyncRequire'),
    babelTransformerPath: require.resolve('@react-native/metro-babel-transformer'),
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  watchFolders: [],
}

Best Practices

Use mergeConfig to preserve React Native defaults:
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
npx react-native start --reset-cache
const isDev = process.env.NODE_ENV !== 'production';
const config = {
  transformer: {
    minifierConfig: {
      compress: { drop_console: !isDev }
    }
  }
};
Add comments explaining why custom configurations are needed.

Next Steps

Start Command

Start Metro development server

Bundle Command

Create production bundles

Metro Docs

Official Metro documentation

Performance

Optimize bundle performance

Build docs developers (and LLMs) love