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:
- Native binaries — the compiled Hermes engine, React Native core, and every native module you've linked.
- JS bundle(s) — your app code plus every npm dependency you
importfrom, compiled to Hermes bytecode. - Assets — images, fonts, Lottie files, video, sound.
- 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 ofimport debounce from 'lodash/debounce'). - Icon libraries that ship every icon by default.
react-native-vector-iconsin 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-fnsorTemporal). - 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-imageon iOS. Savings are typically 25–40% over PNG at equivalent quality. - Use SVG for icons and simple vector art, not
@1x/@2x/@3xPNG 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 -removeon 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:
- Run the bundle visualizer. Delete the top three offenders or replace them with lighter alternatives.
- Convert your five largest PNG assets to WebP and subset your fonts.
- Confirm you're shipping AAB with ABI splits on Android and no Bitcode leftovers on iOS.
- 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.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

In-App Purchases in React Native 2026: RevenueCat, StoreKit 2, and the Receipts That Lie
IAP looks simple until refunds, family sharing, and grace periods hit production. Here's how we wire React Native purchases in 2026 without trusting the client.

The New Architecture in React Native: What Actually Breaks When You Flip It On in 2026
Fabric and TurboModules are the default now, but flipping the switch on a real app is still a minefield. Here's what breaks, what to test first, and how to stage the rollout without shipping a broken build.

Deep Links in React Native 2026: Universal Links, App Links, and the Cold-Start Traps
Deep linking in React Native looks trivial until a marketing team ships a campaign and half the users land on the home screen. Here's what actually breaks in 2026 — and how we route around it.
