Skip to main content

Optimizing App Bundle Size

1. Analyzing Bundle Size

Example: Using webpack-bundle-analyzer

const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static",
      openAnalyzer: false,
      reportFilename: "bundle-report.html"
    })
  ]
};
Tool Description Usage
webpack-bundle-analyzer Visualize the size of webpack bundles npm install --save-dev webpack-bundle-analyzer
react-native-bundle-analyzer Bundle size analyzer for React Native apps npx react-native-bundle-analyzer
Note: The analyzer generates an interactive HTML report to identify large dependencies.
Warning: Bundle size analysis should be performed in release mode to reflect actual app size.

2. Enabling Hermes Engine

Hermes is a JavaScript engine optimized for React Native apps. It improves app startup time, reduces memory usage, and enhances overall performance by compiling JavaScript to bytecode.

Feature Description Status
Hermes Engine JavaScript engine optimized for React Native NEW in React Native 0.60+
Bytecode Compilation Compiles JavaScript to bytecode for faster execution NEW
Startup Time Reduces app startup time by up to 90% NEW
Memory Usage Reduces memory consumption by up to 20% NEW

Example: Enabling Hermes in React Native

// In android/app/build.gradle
project.ext.react = [
    enableHermes: true  // Enable Hermes
]

// In ios/Podfile
use_react_native!(
  :hermes_enabled => true
)
Note: Hermes is enabled by default in React Native 0.60+ for Android and iOS.

3. Using ProGuard/R8

Feature Description Configuration Notes
ProGuard Java bytecode obfuscation and optimization tool for Android apps proguard-rules.pro Used in Android builds to reduce APK size and improve security
R8 Modern replacement for ProGuard, included in Android Gradle Plugin build.gradle Default in Android 12+; more efficient than ProGuard
minifyEnabled Enables code shrinking and obfuscation minifyEnabled true Must be enabled to use ProGuard/R8 rules
shrinkResources Removes unused resources from the final APK shrinkResources true Reduces app size by removing unused assets and drawables

Example: Enabling ProGuard/R8 in build.gradle

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
Note: For React Native apps built with Expo, ProGuard/R8 must be configured in the native Android project after ejecting.
Warning: Incorrect ProGuard rules can break runtime behavior or cause crashes. Always test thoroughly after enabling minification.

4. Implementing Code Splitting

Example: Using React.lazy for Dynamic Imports

import React, { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback="<div>Loading...</div>">
      <HeavyComponent />
    </Suspense>
  );
}
Feature Description Usage
React.lazy Lazily loads components Wrap with Suspense
Suspense Handles loading states for lazy components Must wrap React.lazy
Note: Code splitting with React.lazy works only with functional components.

Example: Splitting by Route

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { lazy } from 'react';

const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

function AppRouter() {
  return (
    <Router>
      <Routes>
        <Route path="/" element=<Home /> />
        <Route path="/about" element=<About /> />
      </Routes>
    </Router>
  );
}

Benefits of Code Splitting

  • Reduces initial bundle size
  • Improves app load time
  • Enables better caching strategies

5. Removing Unused Dependencies

Dependency Usage Bundle Size Impact Removal Recommendation
lodash _.debounce(), _.throttle() ~20KB DEPRECATED Use specific methods or lodash-es
moment.js moment().format() ~20KB NEW Replace with date-fns or dayjs
react-native-vector-icons <Icon name="home" /> ~50KB Remove unused icon sets; use react-native-vector-icons only for required icons
react-navigation useNavigation() ~30KB Use react-navigation only for required navigation features
react-native-gesture-handler GestureHandlerRootView ~15KB Remove if not using gesture handlers

Example: Removing lodash

// Before
import _ from 'lodash';

const debouncedFunction = _.debounce(() => {
  console.log('Debounced');
}, 500);

// After
const debouncedFunction = (func, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func(...args), delay);
  };
};

const debouncedFunction = debouncedFunction(() => {
  console.log('Debounced');
}, 500);

Example: Replacing moment.js with dayjs

// Before
import moment from 'moment';

const formattedDate = moment().format('YYYY-MM-DD');

// After
import dayjs from 'dayjs';

const formattedDate = dayjs().format('YYYY-MM-DD');
Note: Use react-native-vector-icons only for required icons to reduce bundle size.
Warning: Removing dependencies can break functionality if not properly tested.

6. Optimizing Images

Property Description Notes
source Image source to be displayed Can be a local file path or URL
resizeMode How to resize the image to fit its container Options: 'cover', 'contain', 'stretch', 'repeat', 'center'

Example: Using Image with Resize Mode

<Image
  source={require(&#x27;./assets/image.png&#x27;)}
  style={{ width: 200, height: 200 }}
  resizeMode="cover"
/>
Note: Using resizeMode="cover" helps to maintain image quality while fitting within the container.
Image Format Optimization Strategy Notes
.png Use PNG-8 or PNG-24 Smaller file size for simpler graphics
.jpg Compress with appropriate quality Best for complex images like photos
.webp Use for modern devices Smaller file size with better compression

Example: Optimizing Image Size

<Image
  source={{ uri: &#x27;https://example.com/image.webp&#x27; }}
  style={{ width: 300, height: 200 }}
/>

7. Using App Bundles

Example: Configuring App Bundle for Android

android {
    bundle {
        language {
            enableSplit = true
        }
        density {
            enableSplit = true
        }
        abi {
            enableSplit = true
        }
    }
}
Option Description Notes
language.enableSplit Enables splitting by language Reduces bundle size for non-default languages
density.enableSplit Enables splitting by screen density Reduces image size for specific densities
abi.enableSplit Enables splitting by ABI Reduces native library size per device
Note: App bundles are recommended over APKs for better optimization and reduced app size.
Warning: Ensure all dependencies support app bundle format to avoid runtime errors.

8. Enabling Bitcode

Platform Bitcode Support Notes
iOS NEW Supported since iOS 6 Bitcode is enabled by default in Xcode builds. It allows Apple to re-compile your app for optimization purposes.
Android DEPRECATED Not supported Bitcode is an iOS-specific feature and not available on Android.

Example: Enabling Bitcode in Xcode

export ENABLE_BITCODE=YES
Note: Bitcode is only relevant for iOS builds and must be enabled in Xcode project settings or via build configuration.

9. Stripping Debug Symbols

Debug symbols are used during development and debugging to provide detailed information about the code, such as function names, line numbers, and variable names. However, they significantly increase the size of your app bundle and are not needed in production builds.

Symbol Description Impact on Bundle Size
Debug symbols Additional metadata used for debugging and profiling NEW Can increase bundle size by up to 30%

Example: Enabling Debug Symbol Stripping in React Native

cd android
./gradlew assembleRelease -PstripDebugSymbols=true
Note: Stripping debug symbols is a standard practice in production builds to reduce app size and improve performance.

10. Implementing Dynamic Imports

Dynamic imports allow you to split your code into smaller chunks that can be loaded on-demand, reducing the initial bundle size of your React Native app.

Feature Description Usage
import() Asynchronous function that returns a Promise resolving to the module const Module = await import('./Module');

Example: Using Dynamic Imports for Components

import React, { useState } from 'react';

const DynamicComponent = () => {
  const [LoadedComponent, setLoadedComponent] = useState(null);

  const loadComponent = async () => {
    const { default: Component } = await import('./HeavyComponent');
    setLoadedComponent(Component);
  };

  return (
    <div>
      <button onPress={loadComponent}>Load Component</button>
      {LoadedComponent && <LoadedComponent />}
    </div>
  );
};

export default DynamicComponent;
Note: Dynamic imports are supported in React Native through Babel and Metro bundler configurations. Ensure your project is set up to handle ES6 modules correctly.

Was this article helpful?