Skip to content
·7 min read

Your React Native App Bundle Is Too Big — Here's How to Shrink It

A 50MB React Native app is a problem. Users bounce, downloads drop, especially on mobile networks. Here's the bundle size audit I run on every client project.

React NativePerformanceBundle SizeMobile

A 50MB React Native app is a real problem. Users on mobile networks abandon downloads. App store conversion rates drop. Storage-constrained users delete your app first. Reviewers mention the size.

Bundle size is one of the most impactful and most ignored metrics. Here is the audit I run on every client project. Most apps lose 10 to 30 MB without sacrificing features.

Start with measurement

You cannot optimize what you cannot see. Get the bundle visualizer running.

For Expo projects:

npx expo-bundle-visualizer

For bare React Native:

yarn add --dev react-native-bundle-visualizer
yarn run visualizer:android
yarn run visualizer:ios

This generates a treemap showing every module in your bundle and its size. Sort by size and you will see the biggest wins immediately.

The first time I ran this on a client app, we found 90KB of moment.js, 70KB of lodash, and 200KB of unused vector icons. Three hours later, the bundle was 350KB smaller.

Enable Hermes

Hermes is the JavaScript engine optimized for React Native. It produces smaller bundles and runs faster than JSC.

For Expo:

// app.json
{
  "expo": {
    "jsEngine": "hermes"
  }
}

For bare React Native, edit android/app/build.gradle:

project.ext.react = [
    enableHermes: true
]

And in ios/Podfile:

use_react_native!(
  :hermes_enabled => true
)

Hermes alone saves 30 to 50 percent on bundle size and noticeably improves startup time. There is no reason not to use it in 2026.

The usual suspects

I have audited dozens of React Native apps. The same dependencies show up as the biggest wins.

moment.js

Moment.js is the single biggest bundle size offender in JavaScript. It is 67KB minified, not tree-shakeable, and officially deprecated by its own authors.

Replace with date-fns:

// Before
import moment from 'moment'
const formatted = moment(date).format('YYYY-MM-DD')
 
// After
import { format } from 'date-fns'
const formatted = format(date, 'yyyy-MM-dd')

date-fns is tree-shakeable. Importing format brings in only that function. Or use the native Intl.DateTimeFormat API for formatting without any library.

This change alone saves 60-100KB.

lodash

import _ from 'lodash' pulls in the entire library. Even if you use one function.

Two fixes:

  1. Add babel-plugin-lodash and switch to named imports:

    import { debounce } from 'lodash'
  2. Use native methods where possible. Most lodash functions have native equivalents:

    // Before
    _.map(arr, fn)
    _.filter(arr, pred)
    _.reduce(arr, fn, init)
     
    // After
    arr.map(fn)
    arr.filter(pred)
    arr.reduce(fn, init)

Many lodash functions are not needed in modern JS. Audit each usage and use native methods where you can.

Vector icons

react-native-vector-icons is convenient and ships entire icon sets. If you use 10 icons from MaterialCommunityIcons (which has 6,000+ icons), the whole set is in your bundle.

Three alternatives:

  1. Individual SVGs — use react-native-svg and import only the icons you use.
  2. SF Symbols on iOS — use expo-symbols or a native module to access Apple's built-in icon library.
  3. Custom icon font — use tools like IcoMoon to generate a font with only the icons you use.

SVGs are my default. They are crisp, small, and easy to swap.

Unused polyfills

If you support older platforms, your app may include polyfills for features that are now standard. Audit your Babel config and remove polyfills for:

  • Promise (universally supported)
  • fetch (universally supported)
  • Object.assign (universally supported)
  • Array.prototype.includes (supported on iOS 11+, Android 5+)

Large assets bundled as resources

Images bundled as resources add to your bundle size. For non-critical images, load them from a CDN:

// Before — bundled, adds to app size
<Image source={require('./hero.png')} />
 
// After — loaded from CDN, does not add to app size
<Image source={{ uri: 'https://cdn.example.com/hero.png' }} />

For small images that load on every app open (logos, brand assets), bundling is fine. For large marketing images, lazy-load from a CDN.

Code splitting

React Native does not support code splitting the way web does — the bundle is one file. But you can defer loading of expensive modules:

import { useState, lazy, Suspense } from 'react'
 
const HeavyChart = lazy(() => import('./HeavyChart'))
 
function AnalyticsScreen() {
  const [showChart, setShowChart] = useState(false)
 
  return (
    <>
      <Button title="Show chart" onPress={() => setShowChart(true)} />
      {showChart && (
        <Suspense fallback={<Spinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </>
  )
}

This works with Metro's inline require and reduces the initial parse time. The full bundle size is the same, but the user gets to the home screen faster.

ProGuard and R8 (Android)

Android's R8 strips unused code from the bundle automatically. Make sure it is enabled:

// android/app/build.gradle
buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

For libraries that need to keep specific classes (reflection-heavy libraries), add keep rules in proguard-rules.pro.

iOS app thinning

iOS supports app slicing — users download only the assets and code their device needs. Enable this in Xcode:

  1. Set ENABLE_BITCODE to NO (bitcode is deprecated).
  2. Provide appropriately sized image assets for each scale.
  3. Make sure your asset catalog uses @2x and @3x properly.

The target

Aim for these sizes after optimization:

  • Android (AAB): under 25 MB. Excellent if under 15 MB.
  • iOS (IPA): under 35 MB. Excellent if under 25 MB.

Smaller apps download faster, install faster, and convert better on the app store listing. Users judge size before they even open the app.

The checklist

Run this on your app:

  • Hermes enabled
  • moment.js removed (use date-fns or Intl)
  • lodash tree-shaken or removed
  • Vector icons replaced with SVGs or pruned font
  • Unused polyfills removed from Babel config
  • Large images moved to CDN
  • R8 / ProGuard enabled on Android
  • iOS asset catalog uses correct scales
  • Bundle visualizer run, biggest modules audited
  • AAB / IPA size under target

Bundle size is not just about download time. It affects conversion, retention, and the first impression users get before they even see your app.

Need help shrinking your React Native bundle?

I optimize React Native apps for size and startup speed. Let's talk.

Frequently Asked Questions

What is a good React Native bundle size?

For a typical app with a few screens and standard dependencies, aim for under 25MB on Android and 35MB on iOS. Above that, users on mobile networks bounce. Under 15MB is excellent.

Does Hermes reduce React Native bundle size?

Yes, by 30 to 50 percent compared to JavaScriptCore. Hermes also improves app startup time and reduces memory usage. Enable Hermes unless you have a specific reason not to.

How do I see what is in my React Native bundle?

Use react-native-bundle-visualizer or expo-bundle-visualizer. They generate a treemap of every module in your bundle and its size. Use it to find the biggest dependencies.

Why is my React Native app so big?

The usual suspects are: moment.js instead of date-fns, lodash imported wholesale, react-native-vector-icons shipping entire icon sets, unused polyfills, and large images bundled as assets instead of loaded remotely.