All articles
Mobile DevelopmentJuly 27, 2026 7 min read

App Size Bloat in React Native: Where the Megabytes Actually Come From in 2026

A field guide to shrinking React Native app binaries in 2026: Hermes bytecode, native modules, image assets, and the App Store Connect numbers that don't match what you built locally.

Every quarter someone on our team opens App Store Connect, sees a 148 MB download size, and mutters "but the AAB was 42 MB." App size is one of those metrics that everyone tracks and almost nobody understands — because the number you build is not the number your user downloads, and the number your user downloads is not the number that ends up on disk.

Here's an honest breakdown of where React Native apps actually get fat in 2026, what to measure, and which optimizations move the needle versus which ones just make your CI logs look busy.

Why size still matters (and where it doesn't)

On a US-market iPhone with fibre Wi-Fi, nobody cares if your app is 60 MB or 140 MB. On a mid-range Android in Jakarta or São Paulo with a metered connection, the install-completion rate falls off a cliff past roughly 50 MB — Google has published data on this for years, and our own funnel numbers echo it directionally.

There's also the iOS 200 MB cellular download cap. Apple raised the over-the-air limit multiple times, but exceeding it still means users on cellular get a "connect to Wi-Fi" prompt. For consumer apps that's a conversion killer.

And there's a subtler cost: OTA updates via EAS Update or CodePush ship your JS bundle, and if that bundle has ballooned to 12 MB gzipped because someone imported all of lodash and three icon libraries, every update is slower and more likely to fail on poor networks.

The four things that actually take up space

When we audit a React Native app, the bytes almost always live in four buckets:

  1. Native binaries — the compiled Hermes engine, React Native core, and every native module you've linked.
  2. JS bundle(s) — your app code plus every npm dependency you import from, compiled to Hermes bytecode.
  3. Assets — images, fonts, Lottie files, video, sound.
  4. Architectures and locales — the fat you ship because you're building one artifact for many devices.

The fourth one is where the "my AAB is 42 MB but users download 90 MB" confusion usually comes from. Let's walk through each.

Native binaries: Hermes is not the villain

A fresh Expo SDK 52+ app with Hermes enabled ships around 15–20 MB of native code on Android (arm64-v8a slice) and roughly 25–35 MB on iOS after App Store thinning. That's the floor. You cannot get below it without ejecting React Native itself, which nobody should do.

What pushes this number up is native modules. Every time you add a library that pulls in a native SDK — Firebase, Stripe, Datadog RUM, MLKit, Reanimated's worklets runtime, a video player, a maps SDK — you're adding compiled .so files on Android and static libraries on iOS. Individually they're 200 KB to 2 MB. Collectively, they're often 40+ MB.

Run this on Android to see the damage:

cd android
./gradlew :app:bundleRelease
unzip -l app/build/outputs/bundle/release/app-release.aab | \
  sort -k1 -n -r | head -40

On iOS, use Xcode's Organizer → your archive → "Show Package Contents" and look at the .app bundle. Or use bloaty on the binary if you're feeling forensic.

JS bundle: where the sneaky bloat lives

A Hermes bytecode bundle for a mid-sized app should be somewhere between 2 and 6 MB. If yours is 15 MB, something is very wrong, and it's almost always one of these:

  • Importing an entire library when you use one function (import _ from 'lodash' instead of import debounce from 'lodash/debounce').
  • Icon libraries that ship every icon by default. react-native-vector-icons in particular can add several MB if you don't strip unused glyphs.
  • Moment.js locales (yes, in 2026, we still find Moment in codebases — please migrate to date-fns or Temporal).
  • Duplicate versions of the same package due to loose peer deps.
  • Source maps accidentally being included in production.

Get a real picture with react-native-bundle-visualizer:

npx react-native-bundle-visualizer --platform ios --dev false

Or for Expo:

npx expo export --platform ios
npx source-map-explorer dist/_expo/static/js/ios/*.hbc.map

The first time you run this on a legacy app, you will find something horrifying. In one audit we found a client shipping a full copy of a headless CMS's TypeScript SDK — 3.2 MB — because a util file re-exported * from a module that transitively imported it.

Assets: the boring win

Assets are the least glamorous fix and often the biggest one. A single un-optimized PNG splash screen can be 4 MB. A folder of onboarding illustrations at @3x resolution shipped as PNG instead of WebP can easily add 20 MB.

Rules we follow:

  • Use WebP for photos and complex illustrations. React Native supports it natively on Android and via expo-image on iOS. Savings are typically 25–40% over PNG at equivalent quality.
  • Use SVG for icons and simple vector art, not @1x/@2x/@3x PNG sets.
  • Lottie files are JSON. They compress well but people ship them uncompressed, with embedded base64 images, and at animation lengths users never actually watch. Trim them.
  • Fonts: subset them. If you're loading Inter with all weights and italic variants, you're shipping ~1.5 MB of font. Most apps use two weights.
  • Videos don't belong in the bundle. Stream them or download on first launch.

For Expo apps, expo-asset with expo-image handles most of this cleanly. If you're still on Image from react-native, migrate — expo-image also gives you disk caching, which reduces perceived size in a different way.

Architectures and locales: the store-side illusion

Here's the source of the "my build is small but the store says it's huge" confusion.

Android: use App Bundles and split ABIs

An .aab uploaded to Play Console contains code for all CPU architectures (arm64-v8a, armeabi-v7a, x86_64). Play then generates per-device APKs. If you're building a universal APK yourself and distributing it, every user downloads every architecture. Don't do that.

In android/app/build.gradle, make sure:

android {
    splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
            universalApk false
        }
    }
}

And ship AAB, not APK, to production. Play's dynamic delivery cuts install size roughly in half for most apps just by not shipping the wrong architecture.

iOS: App Thinning does most of the work

Apple's App Thinning slices your IPA per-device automatically. The number App Store Connect shows you for "download size" after processing is the honest one — that's what users actually pull down. The "install size" is what ends up on disk after decompression.

What you can control:

  • Strip unused architectures from third-party frameworks. Some vendor SDKs still ship fat binaries with simulator slices. A build phase script that runs lipo -remove on release builds handles this.
  • Enable Bitcode? No. Apple deprecated it. If your project still has ENABLE_BITCODE = YES, remove it.
  • Use asset catalogs, not loose files. Xcode optimizes them, thins them per-device, and applies lossless compression.

Measuring the right number

Set up a size budget in CI. We track four numbers per release:

  • Android AAB size (upload artifact)
  • Android estimated install size for a Pixel-class device (from Play Console API)
  • iOS IPA size (archive)
  • iOS thinned download size for iPhone 15-class device (App Store Connect API)

When a PR pushes any of these over a threshold — we use 5% growth as a soft alarm, 10% as a hard block — the build fails and the author has to justify it. This alone prevented more bloat than every optimization pass we've ever done.

If you want a starting script:

# Fail CI if the AAB grew more than 10%
CURRENT=$(stat -f%z app-release.aab)
BASELINE=$(cat .size-baseline)
GROWTH=$(( (CURRENT - BASELINE) * 100 / BASELINE ))
if [ $GROWTH -gt 10 ]; then
  echo "AAB grew ${GROWTH}%. Blocking."
  exit 1
fi

Where we'd start

If you have one afternoon and an app that feels too big, do this in order:

  1. Run the bundle visualizer. Delete the top three offenders or replace them with lighter alternatives.
  2. Convert your five largest PNG assets to WebP and subset your fonts.
  3. Confirm you're shipping AAB with ABI splits on Android and no Bitcode leftovers on iOS.
  4. Add a size check to CI so this problem doesn't come back next quarter.

That's usually 30–50% off the download size, in one day, without touching a single feature. The deeper native-module audit can wait for the next sprint — and if you want a hand with it, our mobile team has done this dance more times than we'd like to admit.

#React Native#Expo#Performance#Release Ops#iOS#Android

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project