optimize-bundle-size
Reduce app bundle size through code splitting, tree shaking, and asset optimization.
Overview
The optimize-bundle-size command implements various optimization techniques to reduce your React Native app's bundle size, improving download times, installation size, and app performance.
Usage
bash
/template optimize-bundle-size [options]Options
--analyze- Generate bundle analysis report--images- Optimize image assets--fonts- Subset and optimize fonts--split- Enable code splitting--hermes- Enable Hermes engine optimizations
Examples
Basic Optimization
bash
/template optimize-bundle-sizeFull Optimization Suite
bash
/template optimize-bundle-size --analyze --images --fonts --split --hermesAnalysis Only
bash
/template optimize-bundle-size --analyzeWhat It Creates
Optimization Structure
├── metro.config.js # Metro bundler config
├── babel.config.js # Babel optimizations
├── scripts/
│ ├── analyze-bundle.js # Bundle analysis
│ ├── optimize-images.js # Image optimization
│ └── subset-fonts.js # Font subsetting
├── src/
│ ├── utils/
│ │ └── lazy.ts # Lazy loading utilities
│ └── optimizations/
│ ├── dynamic-imports.ts
│ └── asset-loader.tsMetro Configuration
javascript
// metro.config.js
const { getDefaultConfig } = require('@react-native/metro-config');
module.exports = (async () => {
const defaultConfig = await getDefaultConfig(__dirname);
return {
...defaultConfig,
transformer: {
...defaultConfig.transformer,
minifierPath: 'metro-minify-terser',
minifierConfig: {
ecma: 8,
keep_fnames: false,
keep_classnames: false,
module: true,
mangle: {
module: true,
keep_fnames: false,
reserved: [],
},
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info', 'console.debug'],
passes: 3,
global_defs: {
__DEV__: false,
},
},
},
optimizationSizeLimit: 150 * 1024, // 150KB
},
resolver: {
...defaultConfig.resolver,
assetExts: defaultConfig.resolver.assetExts.filter(
ext => ext !== 'svg'
),
sourceExts: [...defaultConfig.resolver.sourceExts, 'svg'],
},
serializer: {
...defaultConfig.serializer,
processModuleFilter: (module) => {
// Remove test files
if (module.path.includes('__tests__')) return false;
if (module.path.includes('.test.')) return false;
if (module.path.includes('.spec.')) return false;
// Remove stories
if (module.path.includes('.stories.')) return false;
// Remove mocks
if (module.path.includes('__mocks__')) return false;
return true;
},
},
};
})();Babel Optimizations
javascript
// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
// Remove console logs in production
['transform-remove-console', {
exclude: ['error', 'warn'],
}],
// Optimize lodash imports
['lodash', {
id: ['lodash', 'recompose'],
}],
// Transform imports for tree shaking
['import', {
libraryName: '@ant-design/react-native',
style: false,
}],
// Inline constants
['transform-inline-environment-variables', {
include: ['NODE_ENV'],
}],
// Dead code elimination
['minify-dead-code-elimination', {
optimizeRawSize: true,
}],
],
env: {
production: {
plugins: [
// Strip flow types
'@babel/plugin-transform-flow-strip-types',
// Optimize React
['transform-react-remove-prop-types', {
mode: 'remove',
removeImport: true,
}],
// Constant folding
'minify-constant-folding',
// Simplify code
'minify-simplify',
],
},
},
};Dynamic Imports
typescript
// src/utils/lazy.ts
import React, { lazy, Suspense, ComponentType } from 'react';
import { View, ActivityIndicator } from 'react-native';
// Lazy load screens
export function lazyScreen<T extends ComponentType<any>>(
importFunc: () => Promise<{ default: T }>
): T {
const LazyComponent = lazy(importFunc);
return ((props: any) => (
<Suspense fallback={<ScreenLoader />}>
<LazyComponent {...props} />
</Suspense>
)) as T;
}
// Loading placeholder
function ScreenLoader() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
// Usage in navigation
export const screens = {
Home: require('../screens/Home').default, // Always loaded
Profile: lazyScreen(() => import('../screens/Profile')),
Settings: lazyScreen(() => import('../screens/Settings')),
// Heavy screens loaded on demand
Analytics: lazyScreen(() => import('../screens/Analytics')),
VideoPlayer: lazyScreen(() => import('../screens/VideoPlayer')),
};Image Optimization
javascript
// scripts/optimize-images.js
const imagemin = require('imagemin');
const imageminPngquant = require('imagemin-pngquant');
const imageminMozjpeg = require('imagemin-mozjpeg');
const imageminSvgo = require('imagemin-svgo');
const sharp = require('sharp');
const glob = require('glob');
const path = require('path');
async function optimizeImages() {
// Find all images
const images = glob.sync('src/assets/images/**/*.{jpg,jpeg,png,svg}');
for (const imagePath of images) {
const ext = path.extname(imagePath).toLowerCase();
const baseName = path.basename(imagePath, ext);
const dir = path.dirname(imagePath);
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg') {
// Generate multiple resolutions
const sizes = [
{ suffix: '', scale: 1 },
{ suffix: '@2x', scale: 2 },
{ suffix: '@3x', scale: 3 },
];
for (const { suffix, scale } of sizes) {
const outputPath = path.join(dir, `${baseName}${suffix}${ext}`);
await sharp(imagePath)
.resize(null, null, {
width: Math.round(100 * scale),
withoutEnlargement: true,
})
.toFile(outputPath);
}
}
// Optimize with imagemin
await imagemin([imagePath], {
destination: dir,
plugins: [
imageminPngquant({
quality: [0.6, 0.8],
speed: 1,
strip: true,
}),
imageminMozjpeg({
quality: 75,
progressive: true,
}),
imageminSvgo({
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'cleanupIDs', active: true },
],
}),
],
});
}
console.log(`Optimized ${images.length} images`);
}
optimizeImages();Font Subsetting
javascript
// scripts/subset-fonts.js
const Fontmin = require('fontmin');
const glob = require('glob');
const fs = require('fs');
async function subsetFonts() {
// Get all unique characters used in the app
const jsFiles = glob.sync('src/**/*.{js,jsx,ts,tsx}');
const allText = jsFiles
.map(file => fs.readFileSync(file, 'utf8'))
.join('');
// Extract unique characters
const uniqueChars = [...new Set(allText)].join('');
// Subset fonts
const fontmin = new Fontmin()
.src('src/assets/fonts/*.ttf')
.dest('src/assets/fonts/subset')
.use(Fontmin.glyph({
text: uniqueChars,
hinting: false,
}))
.use(Fontmin.ttf2woff2());
await fontmin.run();
console.log('Fonts subsetted successfully');
}
subsetFonts();Bundle Analysis
javascript
// scripts/analyze-bundle.js
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
function analyzeBundle() {
// Generate bundle
console.log('Generating bundle...');
execSync(
'npx react-native bundle ' +
'--platform ios ' +
'--dev false ' +
'--entry-file index.js ' +
'--bundle-output ./bundle-analysis/main.jsbundle ' +
'--assets-dest ./bundle-analysis',
{ stdio: 'inherit' }
);
// Analyze with source-map-explorer
console.log('Analyzing bundle...');
execSync(
'npx source-map-explorer ./bundle-analysis/main.jsbundle ' +
'--html ./bundle-analysis/report.html',
{ stdio: 'inherit' }
);
// Generate size report
const stats = fs.statSync('./bundle-analysis/main.jsbundle');
const sizeInMB = (stats.size / 1024 / 1024).toFixed(2);
console.log(`\nBundle size: ${sizeInMB} MB`);
console.log('Report generated at: ./bundle-analysis/report.html');
}
analyzeBundle();Code Splitting Implementation
typescript
// src/optimizations/dynamic-imports.ts
export class DynamicImporter {
private cache = new Map<string, any>();
async importModule(moduleName: string) {
if (this.cache.has(moduleName)) {
return this.cache.get(moduleName);
}
let module;
switch (moduleName) {
case 'heavy-chart-library':
module = await import('heavy-chart-library');
break;
case 'pdf-viewer':
module = await import('react-native-pdf');
break;
case 'video-processing':
module = await import('./videoProcessing');
break;
default:
throw new Error(`Unknown module: ${moduleName}`);
}
this.cache.set(moduleName, module);
return module;
}
}
// Usage
const importer = new DynamicImporter();
export function ChartScreen() {
const [ChartLib, setChartLib] = useState(null);
useEffect(() => {
importer.importModule('heavy-chart-library').then(lib => {
setChartLib(lib);
});
}, []);
if (!ChartLib) {
return <ActivityIndicator />;
}
return <ChartLib.LineChart data={data} />;
}Hermes Optimization
javascript
// android/app/build.gradle
project.ext.react = [
enableHermes: true,
hermesCommand: "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc",
// Enable Hermes optimizations
hermesFlagsRelease: [
"-O",
"-output-source-map",
"-emit-binary",
"-max-diagnostic-width=80",
"-w",
],
]
// iOS - Podfile
use_react_native!(
:hermes_enabled => true,
:fabric_enabled => false,
# Hermes optimization flags
:hermes_enabled_release => true,
)Asset Loading Optimization
typescript
// src/optimizations/asset-loader.ts
import { Image } from 'react-native';
import FastImage from 'react-native-fast-image';
export class AssetLoader {
private imageCache = new Map<string, any>();
// Preload critical images
async preloadImages(urls: string[]) {
const promises = urls.map(url =>
FastImage.preload([{ uri: url }])
);
await Promise.all(promises);
}
// Load image on demand
loadImage(source: any) {
const key = typeof source === 'string' ? source : source.uri;
if (!this.imageCache.has(key)) {
this.imageCache.set(key, source);
Image.prefetch(key);
}
return this.imageCache.get(key);
}
// Clear unused assets
clearUnusedAssets(activeScreens: string[]) {
// Implementation depends on navigation tracking
}
}Platform-Specific Optimizations
Android ProGuard Rules
# android/app/proguard-rules.pro
-keep class com.facebook.hermes.unicode.** { *; }
-keep class com.facebook.jni.** { *; }
# Remove debugging information
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}
# Optimize enums
-optimizations !code/simplification/cast,!field/*,!class/merging/*,code/allocation/variableiOS Build Settings
ruby
# ios/Podfile
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if config.name == 'Release'
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-O'
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = 's'
config.build_settings['ENABLE_BITCODE'] = 'YES'
config.build_settings['DEAD_CODE_STRIPPING'] = 'YES'
end
end
end
endMonitoring Bundle Size
Size Budget
json
// package.json
{
"bundlesize": [
{
"path": "./android/app/build/outputs/apk/release/*.apk",
"maxSize": "30 MB"
},
{
"path": "./ios/build/*.app",
"maxSize": "40 MB"
}
]
}CI Integration
yaml
# .github/workflows/bundle-size.yml
- name: Check Bundle Size
run: |
npm run build:release
npm run bundlesizeBest Practices
- Measure First: Always analyze before optimizing
- Progressive Loading: Load features as needed
- Image Optimization: Use appropriate formats and sizes
- Remove Unused Code: Regular dependency audits
- Monitor Continuously: Track size over time
Results Tracking
Before/After Metrics
typescript
// Track optimization results
export interface OptimizationMetrics {
bundleSize: {
before: number;
after: number;
reduction: string;
};
images: {
count: number;
totalSizeBefore: number;
totalSizeAfter: number;
};
performance: {
startupTime: number;
memoryUsage: number;
};
}Related Commands
add-screen- Implement lazy loadingsetup-crash-reporting- Monitor app performanceadd-offline-sync- Optimize offline storage
