HomeGuidesReferenceLearn

Metro bundler

Learn about different Metro bundler configurations that can be customized.


Expo CLI uses Metro during npx expo start and npx expo export to bundle your JavaScript code and assets. Metro is built and optimized for React Native, and used for large-scale applications such as Facebook and Instagram.

Customizing

You can customize the Metro bundler by creating a metro.config.js file at the root of your project. This file should export a Metro configuration that extends expo/metro-config. Import expo/metro-config instead of @expo/metro-config to ensure version consistency.

Run the following command to generate the template file:

Terminal
npx expo customize metro.config.js

The metro.config.js file looks as below:

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

module.exports = config;

See metro.config.js documentation for more information.

Assets

Metro resolves files as either source code or assets. Source code is JavaScript, TypeScript, JSON, and other files used by your application. Assets are images, fonts, and other files that should not be transformed by Metro. To accommodate large-scale codebases, Metro requires all extensions for both source code and assets to be explicitly defined before starting the bundler. This is done by adding the resolver.sourceExts and resolver.assetExts options to the Metro configuration. By default, the following extensions are included:

  • resolver.assetExts
  • resolver.sourceExts

Adding more file extensions to assetExts

The most common customization is to include extra asset extensions to Metro.

In the metro.config.js file, add the file extension (without a leading .) to resolver.assetExts array:

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

config.resolver.assetExts.push(
  // Adds support for `.db` files for SQLite databases
  'db'
);

module.exports = config;

Tree shaking by mode

To strip code in production or development, use the process.env.NODE_ENV environment variable. For example:

Input
if (process.env.NODE_ENV === 'development') {
  console.log('Hello in development');
}
Output (development)
'development' === process.env.NODE_ENV && console.log('Hello in development');
Output (production)
%%placeholder-start%%Empty file %%placeholder-end%%

Metro also supports removing code based on the __DEV__ global boolean, however this is less common in the JavaScript ecosystem.

Tree shaking by platform

To remove code based on the Platform module from react-native, you need to add the following to your metro.config.js:

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

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

module.exports = config;

And configure the babel.config.js to preserve import/export syntax:

babel.config.js
module.exports = function (api) {
  api.cache(true);
  const disableImportExportTransform = true;
  return {
    presets: [
      [
        'babel-preset-expo',
        {
          native: {
            disableImportExportTransform,
          },
          web: {
            disableImportExportTransform,
          },
        },
      ],
    ],
  };
};

Now the following transformation input:

Input
import { Platform } from 'react-native';

if (Platform.OS === 'ios') {
  console.log('Hello on iOS');
}

Will produce the following output:

Output (iOS)
console.log('Hello on iOS');
Output (Android)
%%placeholder-start%%Empty on Android %%placeholder-end%%

Minification

The source code is optimized for readability and debugging. To optimize your application for performance, the source code is automatically minified when compiling and exporting. You can also minify your code during development with npx expo start --minify. This is sometimes useful for testing production optimizations.

Terser

terser is the default minifier in SDK 48 and above: (Metro@0.73.0 changelog).

For projects SDK 47 and below, you can use terser instead of uglify-es to mangle and compress your project.

1

Install Terser in your project by running the following command:

Terminal
yarn add --dev metro-minify-terser

2

Set Terser as a minifier with transformer.minifierPath, and pass in terser options via transformer.minifierConfig.

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

config.transformer.minifierPath = 'metro-minify-terser';
config.transformer.minifierConfig = {
  // Terser options...
};

module.exports = config;

Minification collapses whitespace, removes comments, and shortens static operations. For example, the following code:

Input
// This comment will be stripped
console.log('a' + ' ' + 'long' + ' string' + ' to ' + 'collapse');

Is minified to:

Output
console.log('a long string to collapse');

Comments can be preserved by using the /** @preserve */ directive.

Unsafe Terser options

For additional compression that may not work in all JavaScript engines, you can enable the unsafe compress options:

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

config.transformer.minifierPath = 'metro-minify-terser';

config.transformer.minifierConfig = {
  compress: {
    // Enable all unsafe optimizations.
    unsafe: true,
    unsafe_arrows: true,
    unsafe_comps: true,
    unsafe_Function: true,
    unsafe_math: true,
    unsafe_symbols: true,
    unsafe_methods: true,
    unsafe_proto: true,
    unsafe_regexp: true,
    unsafe_undefined: true,
    unused: true,
  },
};

module.exports = config;

esbuild

You can use esbuild to minify exponentially faster than uglify-es and terser. For more information, see metro-minify-esbuild usage.

Uglify

uglify-es is the default minifier in SDK 47 and below, this used the following options.

For projects SDK 48 and above, you can use uglify-es like so:

1

Install Uglify in your project by running the following command:

Terminal
yarn add --dev metro-minify-uglify

Ensure the version of metro-minify-uglify matches the version of metro in your project.

2

Set Uglify as a minifier with transformer.minifierPath, and pass in options to transformer.minifierConfig.

metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

config.transformer.minifierPath = 'metro-minify-uglify';
config.transformer.minifierConfig = {
  // Options: https://github.com/mishoo/UglifyJS#compress-options
};

module.exports = config;

Source map exploration

A useful way to debug your source code is by exploring the source maps with react-native-bundle-visualizer.

To use it, run the following command:

Terminal
npx react-native-bundle-visualizer

This will show an interactive breakdown of what makes up your JavaScript bundle. Using this, you can find large packages that you may not have intended to bundle into your project. The smaller the bundle, the faster your app will start.

Web support

Metro web support is under development and only works with the local Expo CLI and Expo SDK 46 or newer. It's recommended you use it with Expo Router.

By default, Expo CLI uses webpack as the bundler on web platforms because Metro historically did not support web. Using different bundlers across platforms leads to some critical divergence in how an app works across platforms. Features such as Fast Refresh which works on native don't work on the web, and important production functionality such as assets are treated differently across bundlers.

By utilizing Metro across all platforms you can have a more universal development experience. You also get to utilize shared cached chunks across platforms meaning faster iteration speed when working across platforms. Project upgrades can also be easier since there are fewer dependencies (webpack, webpack-dev-server) you need to update between versions.

All Metro web features should be universal (bundling, static files, assets) making the DX easier to understand and faster across the app development process.

Learn once, bundle everywhere!

Expo webpack versus Expo Metro

Universal Expo Metro is designed to be fully universal, meaning any web bundling features should also work on native too. Because of this, we make some breaking changes between the two bundler implementations, carefully check the difference if you're moving from webpack to Metro.

FeatureMetrowebpack
Start commandnpx expo startnpx expo start
Bundle commandnpx expo exportnpx expo export:web
Output folderdistweb-build
Static folderpublicweb
Config filemetro.config.jswebpack.config.js
Default config@expo/metro-config@expo/webpack-config
Multi-platform
Fast Refresh (@expo/metro-runtime)
Error Overlay (@expo/metro-runtime)
Deferred Bundling (@expo/metro-runtime)
Global CSS (Beta • SDK 49)
CSS Modules (Beta • SDK 49)
Bundle Splitting (pending native)
Tree Shaking
Asset Manifests

Note that aliases, resolution, and other bundler features are now universal across platforms as well!

Adding Web support to Metro

To enable Metro web support, make sure your project is using Expo SDK 46 or newer. Then modify your app config to enable the feature using the expo.web.bundler field:

app.json
{
  "expo": {
    "web": {
      "bundler": "metro"
    }
  }
}

Development

To start the development server run the following command:

Terminal
npx expo start --web

Alternatively, press W in the Expo CLI terminal UI.

Production

Tip Use npx expo export instead of npx expo export:web or expo build:web.

You can bundle the project for hosting as it is done for native:

Terminal
npx expo export

In the terminal, after running the above command, you'll see a similar output as shown below:

$ npx expo export
Starting Metro Bundler
iOS ./index.tsx ░░░░░░░░░░░░░░░░  4.0% (  8/132)
Android ./index.tsx ░░░░░░░░░░░░░░░░  0.5% (  3/129)
Web ./index.tsx ░░░░░░░░░░░░░░░░  4.0% ( 5/5)

You can also skip bundling for other platforms by using the --platform flag:

Terminal
npx expo export --platform web

The output is found in the dist directory. To test it locally and see it will work in production, use Serve CLI.

Terminal
npx serve dist

You can deploy the website using any popular web host by following a guide from publishing websites, and substitute the web-build directory for dist.

Static files

Expo's Metro implementation supports hosting static files from the dev server by putting them in the root public/ directory. It is similar to many other web frameworks. In Expo webpack, the web directory is default.

When exporting with npx expo export, the contents of the public directory are copied into the dist/ directory. It means your app can expect to fetch these assets relative to the host URL. The most common example of this is the public/favicon.ico which is used by websites to render the tab icon.

You can overwrite the default index.html in Metro web by creating a public/index.html file in your project.

In the future, this will work universally across platforms with EAS Update hosting. Currently, the feature is web-only based on the static host used for the native app, for example, the legacy Expo service updates do not support this feature.

TypeScript

Available in SDK 49 and higher.

Expo's Metro config has experimental support for the compilerOptions.paths and compilerOptions.baseUrl fields in the project's tsconfig.json (or jsconfig.json) file. This enables absolute imports and aliases in the project. Learn more in the TypeScript guide.

These feature requires additional setup in bare projects. See the versioned Metro setup guide for more information.

CSS

Available in SDK 49 and higher.

Expo's Metro config offers support for CSS on web. See the versioned Metro CSS guide for more information.

  • Ask a question on the forums

  • Edit this page

Was this doc helpful?