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('./assets/image.png')}
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: 'https://example.com/image.webp' }}
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. |
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.