wwwwwwwwwwwwwwwwwww

Metro Mode

Using Metro bundler for more stable native builds

Overview

One supports using Metro as the bundler for native builds, providing a more stable and battle-tested bundling experience. The default native bundler now uses Rolldown’s experimental dev() API (alpha), while Metro mode offers production-ready stability by leveraging React Native’s standard bundler.

Why Use Metro Mode?

  • Production Stability: Metro is the official React Native bundler with years of production use
  • Better Compatibility: Works seamlessly with all React Native packages and libraries
  • Familiar Tooling: If you’re coming from standard React Native development, Metro works as expected
The Rolldown-based native pipeline is now the default in Vite mode (alpha). It provides fast dev builds with HMR via React Refresh, and production builds with treeshaking. It requires Hermes V1. Metro mode remains recommended for production applications.

Enabling Metro Mode

Configure the native bundler to Metro in your vite.config.ts:

vite.config.ts
import type { UserConfig } from 'vite'
import { one } from 'one/vite'
export default {
plugins: [
one({
native: {
// Enable Metro bundler for native builds
bundler: 'metro',
},
}),
],
} satisfies UserConfig

Limitations

When using Metro mode, only some of the Vite features are supported. One provides only a subset of Vite functionality in Metro mode to maintain compatibility and stability.

Metro mode intentionally supports only a subset of Vite configurations and features. This approach helps avoid implementation differences between Vite and Metro that could cause compatibility issues with React Native packages.
We are actively working on expanding Vite feature support, but for any features we don’t yet provide through Vite configuration, you can always use the standard React Native way to change the configuration using metro.config.js.

Only the following Vite features and configurations are supported in Metro mode for now:

  • import.meta.env Built-in Constants

    Partially Supported

    : supported but may not work as expected for web specific variables.
  • import.meta.env Env Variables

    Supported

  • tsconfig-paths

    Partially Supported

    : As one automatically configures vite-tsconfig-paths to support path mappings defined in tsconfig.json for your project, the same applies to Metro mode. However, if you are using the vite-tsconfig-paths plugin manually, the custom options you provide there will not be applied for Metro.

Configuration

By default, the Metro bundler is configured with Expo’s defaults.

We recommend using the standard Vite way for configuring if possible, as it provides a more consistent experience across web and native builds, and will be more future-proof to switch between bundlers.

Like ordinary React Native apps, you can still configure Metro using metro.config.js for more advanced settings, such as resolver or watcher options:

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

metro.config.js works exactly as it would in a standard React Native project, giving you full control over the bundling process.

No Babel

By default, One replaces Expo’s Babel transformer with its own worker built around Oxc. The worker combines Oxc’s Rust parser and transforms with TypeScript implementations of One’s native transforms, Hermes parsing for Flow, and esbuild for syntax lowering. This pipeline runs without Babel; it is not a standalone Rust bundler. babel.config.js and babel-preset-expo do not configure this worker.

A Babel plugin One has no port for would silently do nothing, so instead the build fails and names it. You have three options:

  • Port it and list it in native.bundlerOptions.nativeTransformModules. Each entry is a module id (Metro runs transforms in separate worker processes, so it has to be serializable) that default-exports (code, ctx) => string | null. Your transforms run first, on the original source, with an absolute ctx.filename.
  • Remove it from native.bundlerOptions.babelConfigOverrides.
  • Set native.bundlerOptions.nativeTransforms: false to go back to the Babel transformer, or ONE_METRO_NATIVE_TRANSFORMS=0 for a single run.

A native transform receives source text and returns updated source text. It can use Oxc or another tool suited to the transformation:

scripts/my-transform.cjs
module.exports = function myTransform(code, ctx) {
// ctx: { filename, platform, dev, projectRoot }
if (!code.includes('__BUILD_ID')) return null
return code.replace(/__BUILD_ID/g, JSON.stringify(process.env.BUILD_ID))
}
vite.config.ts
one({
native: {
bundler: 'metro',
bundlerOptions: {
nativeTransformModules: ['./scripts/my-transform.cjs'],
},
},
})

Return null to leave the file alone. Transforms run in order, before One’s built-in transforms. The first receives the original file contents; each subsequent transform receives the previous transform’s output. A transform that records original source positions should run first.

Lazy Startup

By default, Metro starts as soon as the Vite dev server is ready. If you’re primarily doing web development but have Metro configured, you can defer Metro startup until a native client actually connects:

vite.config.ts
import type { UserConfig } from 'vite'
import { one } from 'one/vite'
export default {
plugins: [
one({
native: {
bundler: 'metro',
bundlerOptions: {
startup: 'lazy',
},
},
}),
],
} satisfies UserConfig

Or via environment variable:

Terminal window
ONE_METRO_LAZY=1 one dev

With lazy startup:

  • Metro won’t start when you run one dev
  • When a simulator or device connects and checks /status, it gets an immediate response while Metro starts in the background
  • The first bundle request waits for Metro to finish initializing

This can significantly speed up dev server startup when you don’t always need native builds.

Performance

Metro mode is designed for production stability, but it may not match the performance of Vite, which bundles with Rolldown. Both paths transform with oxc and run no Babel. We observed that Metro mode provides better memory efficiency.

Edit this page on GitHub.