Skip to content

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-size

Full Optimization Suite

bash
/template optimize-bundle-size --analyze --images --fonts --split --hermes

Analysis Only

bash
/template optimize-bundle-size --analyze

What 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.ts

Metro 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/variable

iOS 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
end

Monitoring 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 bundlesize

Best Practices

  1. Measure First: Always analyze before optimizing
  2. Progressive Loading: Load features as needed
  3. Image Optimization: Use appropriate formats and sizes
  4. Remove Unused Code: Regular dependency audits
  5. 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;
  };
}

Built with ❤️ for the AI Coding community, by Praney Behl