Metro Bundler Expert
Comprehensive expertise in React Native's Metro bundler, including configuration, optimization, custom transformers, caching strategies, and troubleshooting common bundling issues.
What I Know
Metro Fundamentals
What is Metro?
-
JavaScript bundler for React Native
-
Transforms and bundles JavaScript modules
-
Handles assets (images, fonts, etc.)
-
Provides fast refresh for development
-
Generates source maps for debugging
Key Concepts
-
Transformer: Converts source code (TypeScript, JSX) to JavaScript
-
Resolver: Locates modules in the file system
-
Serializer: Combines modules into bundles
-
Cache: Speeds up subsequent builds
Metro Configuration
Basic metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname);
module.exports = config;
Custom Configuration
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
const config = { transformer: { // Enable Babel transformer babelTransformerPath: require.resolve('react-native-svg-transformer'),
// Source map options
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
resolver: { // Custom asset extensions assetExts: defaultConfig.resolver.assetExts.filter(ext => ext !== 'svg'),
// Custom source extensions
sourceExts: [...defaultConfig.resolver.sourceExts, 'svg', 'cjs'],
// Node module resolution
nodeModulesPaths: [
'./node_modules',
'../../node_modules', // For monorepos
],
// Custom platform-specific extensions
platforms: ['ios', 'android', 'native'],
},
server: { // Custom port port: 8081,
// Enhanced logging
enhanceMiddleware: (middleware) => {
return (req, res, next) => {
console.log(`Metro request: ${req.url}`);
return middleware(req, res, next);
};
},
},
watchFolders: [ // Watch external folders (monorepos) path.resolve(__dirname, '..', 'shared-library'), ],
resetCache: true, // Reset cache on start (dev only) };
module.exports = mergeConfig(defaultConfig, config);
Optimization Strategies
Inline Requires
// metro.config.js module.exports = { transformer: { getTransformOptions: async () => ({ transform: { inlineRequires: true, // Lazy load modules (faster startup) }, }), }, };
// Before (eager loading) import UserProfile from './UserProfile'; import Settings from './Settings';
function App() { return ( <View> {showProfile ? <UserProfile /> : <Settings />} </View> ); }
// After inline requires (lazy loading) function App() { return ( <View> {showProfile ? <require('./UserProfile').default /> : <require('./Settings').default /> } </View> ); }
Bundle Splitting (Experimental)
// metro.config.js module.exports = { serializer: { createModuleIdFactory: () => { // Generate stable module IDs for better caching return (path) => { return require('crypto') .createHash('sha1') .update(path) .digest('hex') .substring(0, 8); }; }, }, };
Asset Optimization
// metro.config.js module.exports = { transformer: { // Minify assets minifierPath: require.resolve('metro-minify-terser'), minifierConfig: { compress: { drop_console: true, // Remove console.log in production drop_debugger: true, }, output: { comments: false, }, }, },
resolver: { // Optimize asset resolution assetExts: [ 'png', 'jpg', 'jpeg', 'gif', 'webp', // Images 'mp3', 'wav', 'm4a', 'aac', // Audio 'mp4', 'mov', // Video 'ttf', 'otf', 'woff', 'woff2', // Fonts ], }, };
Custom Transformers
SVG Transformer
Install
npm install react-native-svg react-native-svg-transformer
metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.transformer = { ...config.transformer, babelTransformerPath: require.resolve('react-native-svg-transformer'), };
config.resolver = { ...config.resolver, assetExts: config.resolver.assetExts.filter(ext => ext !== 'svg'), sourceExts: [...config.resolver.sourceExts, 'svg'], };
module.exports = config;
// Usage in code import Logo from './assets/logo.svg';
function App() { return <Logo width={120} height={40} />; }
Multiple File Extensions
// metro.config.js module.exports = { resolver: { // Add .web.js, .native.js for platform-specific code sourceExts: ['js', 'json', 'ts', 'tsx', 'jsx', 'web.js', 'native.js'],
// Custom resolution logic
resolveRequest: (context, moduleName, platform) => {
if (moduleName === 'my-module') {
// Custom module resolution
return {
filePath: '/custom/path/to/module.js',
type: 'sourceFile',
};
}
return context.resolveRequest(context, moduleName, platform);
},
}, };
Caching Strategies
Cache Management
Clear Metro cache
npx react-native start --reset-cache npm start -- --reset-cache # Expo
Or manually
rm -rf $TMPDIR/react-* rm -rf $TMPDIR/metro-*
Clear watchman cache
watchman watch-del-all
Clear all caches (nuclear option)
npm run clear # If configured in package.json
Cache Configuration
// metro.config.js const path = require('path');
module.exports = { cacheStores: [ // Custom cache directory { get: (key) => { const cachePath = path.join(__dirname, '.metro-cache', key); // Implement custom cache retrieval }, set: (key, value) => { const cachePath = path.join(__dirname, '.metro-cache', key); // Implement custom cache storage }, }, ],
// Reset cache on config changes resetCache: process.env.RESET_CACHE === 'true', };
Monorepo Setup
Workspaces Configuration
// metro.config.js (in app directory) const path = require('path'); const { getDefaultConfig } = require('@react-native/metro-config');
const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, '../..');
const config = getDefaultConfig(projectRoot);
// Watch workspace directories config.watchFolders = [workspaceRoot];
// Resolve modules from workspace config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(workspaceRoot, 'node_modules'), ];
// Avoid hoisting issues config.resolver.disableHierarchicalLookup = false;
module.exports = config;
Symlink Handling
// metro.config.js module.exports = { resolver: { // Enable symlink support unstable_enableSymlinks: true,
// Resolve symlinked packages
resolveRequest: (context, moduleName, platform) => {
const resolution = context.resolveRequest(context, moduleName, platform);
if (resolution && resolution.type === 'sourceFile') {
// Resolve real path for symlinks
const realPath = require('fs').realpathSync(resolution.filePath);
return {
...resolution,
filePath: realPath,
};
}
return resolution;
},
}, };
Common Issues & Solutions
"Unable to resolve module"
Solution 1: Clear cache
npx react-native start --reset-cache
Solution 2: Reinstall dependencies
rm -rf node_modules npm install
Solution 3: Check import paths
Ensure case-sensitive imports match file names
import UserProfile from './userProfile'; # ❌ Wrong case import UserProfile from './UserProfile'; # ✅ Correct
Solution 4: Add to metro.config.js
module.exports = { resolver: { extraNodeModules: { 'my-module': path.resolve(__dirname, 'node_modules/my-module'), }, }, };
"Port 8081 already in use"
Find and kill process
lsof -ti:8081 | xargs kill -9
Or start on different port
npx react-native start --port 8082
Update code to use new port
adb reverse tcp:8082 tcp:8082 # Android
"Invariant Violation: Module AppRegistry is not a registered callable module"
Clear all caches
rm -rf $TMPDIR/react-* rm -rf $TMPDIR/metro-* watchman watch-del-all rm -rf node_modules npm install npx react-native start --reset-cache
"TransformError: ... SyntaxError"
// Add Babel plugin to metro.config.js module.exports = { transformer: { babelTransformerPath: require.resolve('./customBabelTransformer.js'), }, };
// customBabelTransformer.js module.exports = require('metro-react-native-babel-preset');
When to Use This Skill
Ask me when you need help with:
-
Configuring Metro bundler
-
Custom transformers (SVG, images, etc.)
-
Optimizing bundle size and startup time
-
Setting up monorepo with Metro
-
Troubleshooting "Unable to resolve module" errors
-
Clearing Metro cache effectively
-
Configuring source maps
-
Platform-specific file resolution
-
Debugging bundling performance
-
Custom asset handling
-
Port conflicts (8081)
-
Symlink resolution in monorepos
Essential Commands
Development
Start Metro bundler
npx react-native start
Start with cache cleared
npx react-native start --reset-cache
Start with custom port
npx react-native start --port 8082
Start with verbose logging
npx react-native start --verbose
Expo dev server
npx expo start
Expo with cache cleared
npx expo start -c
Debugging
Check Metro status
curl http://localhost:8081/status
Get bundle (for debugging)
curl http://localhost:8081/index.bundle?platform=ios > bundle.js
Check source map
curl http://localhost:8081/index.map?platform=ios > bundle.map
List all modules in bundle
curl http://localhost:8081/index.bundle?platform=ios&dev=false&minify=false
Cache Management
Clear Metro cache
rm -rf $TMPDIR/react-* rm -rf $TMPDIR/metro-*
Clear watchman
watchman watch-del-all
Clear all (comprehensive)
npm run clear # Custom script
Or manually:
rm -rf $TMPDIR/react-* rm -rf $TMPDIR/metro-* watchman watch-del-all rm -rf node_modules npm install
Pro Tips & Tricks
- Bundle Analysis
Analyze bundle size to find optimization opportunities:
Generate bundle with source map
npx react-native bundle
--platform ios
--dev false
--entry-file index.js
--bundle-output ./bundle.js
--sourcemap-output ./bundle.map
Analyze with source-map-explorer
npm install -g source-map-explorer source-map-explorer bundle.js bundle.map
- Environment-Specific Configuration
// metro.config.js const isDev = process.env.NODE_ENV !== 'production';
module.exports = { transformer: { minifierConfig: { compress: { drop_console: !isDev, // Remove console.log in production }, }, },
serializer: { getModulesRunBeforeMainModule: () => [ // Polyfills for production ...(!isDev ? [require.resolve('./polyfills.js')] : []), ], }, };
- Custom Asset Pipeline
// metro.config.js module.exports = { transformer: { // Optimize images during bundling assetPlugins: ['expo-asset/tools/hashAssetFiles'], },
resolver: { assetExts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'],
// Custom asset resolution
resolveAsset: (dirPath, assetName, extension) => {
const basePath = `${dirPath}/${assetName}`;
// Try @2x, @3x variants
const variants = ['@3x', '@2x', ''];
for (const variant of variants) {
const path = `${basePath}${variant}.${extension}`;
if (require('fs').existsSync(path)) {
return path;
}
}
return null;
},
}, };
- Preloading Heavy Modules
// index.js import { AppRegistry } from 'react-native'; import App from './App';
// Preload heavy modules import('./src/heavyModule').then(() => { console.log('Heavy module preloaded'); });
AppRegistry.registerComponent('MyApp', () => App);
- Development Performance Boost
// metro.config.js const isDev = process.env.NODE_ENV !== 'production';
module.exports = { transformer: { // Skip minification in dev minifierPath: isDev ? undefined : require.resolve('metro-minify-terser'),
// Faster source maps in dev
getTransformOptions: async () => ({
transform: {
inlineRequires: !isDev, // Only in production
},
}),
},
server: { // Increase file watching performance watchFolders: isDev ? [] : undefined, }, };
Integration with SpecWeave
Configuration Management
-
Document Metro configuration in docs/internal/architecture/
-
Track bundle size across increments
-
Include bundling optimization in tasks.md
Performance Monitoring
-
Set bundle size thresholds
-
Track startup time improvements
-
Document optimization strategies
Troubleshooting
-
Maintain runbook for common Metro issues
-
Document cache clearing procedures
-
Track bundling errors in increment reports