App Size Bloat in React Native: How We Got an Expo App Back Under 40 MB
An Expo app crept past 120 MB on Android. Here's the audit we ran, the libraries we ripped out, and the build flags that actually moved the needle.

One of our clients pinged us last quarter: their Expo app had quietly grown from a healthy 28 MB install to a 122 MB monster on mid-range Android. Install conversion was sliding, and the Play Console was flashing yellow on the size advisory. This is the audit we ran, what we cut, and the build settings that actually mattered.
Why size matters more in 2026 than it did in 2020
Apple's over-the-air install cap is still 200 MB at the time of writing, and Google Play has tightened recommendations on base APK delivery via the Android App Bundle. But the bigger pressure is conversion: every additional 10 MB on Android measurably hurts install completion in markets where data is metered or storage is tight. For us, the rule of thumb is simple — if you're shipping a consumer app to anywhere outside North America and Western Europe, treat 50 MB as a soft ceiling and 100 MB as a hard one.
React Native and Expo make it very easy to blow past that. The defaults are fine. The trouble starts when you add seven animation libraries, ship debug symbols by accident, and bundle the entire lodash package because someone needed debounce.
Step 1: measure before you cut
You cannot optimise what you cannot see. Before touching anything, we pulled three artifacts:
- The release AAB (Android App Bundle) and a generated universal APK from it.
- The release IPA, unzipped.
- The Metro/Hermes JavaScript bundle, output as a source map.
For the JS bundle, the single most useful tool is react-native-bundle-visualizer or, if you're on Expo, generating the bundle manually and feeding it to source-map-explorer.
npx expo export --platform android --output-dir dist
npx source-map-explorer \
dist/_expo/static/js/android/*.hbc \
dist/_expo/static/js/android/*.map
For the native side on Android, unzip the AAB and look at base/lib/ per ABI, plus base/assets/. On iOS, unzip the .ipa, right-click the .app, and inspect contents — pay particular attention to Frameworks/ and any embedded .bundle resources.
In our case, the breakdown looked roughly like this:
- Native libraries (
.sofiles): ~58 MB - JS bundle (Hermes bytecode): ~9 MB
- Image and font assets: ~31 MB
- Fonts alone: ~14 MB
- Everything else: ~24 MB
That distribution told us almost immediately where to spend time.
Read the bundle, not the package.json
package.json lies. It tells you what's installed, not what ships. A library can be 4 MB on disk and contribute 40 KB to the final bundle if tree-shaking works. The opposite is also true: a 200 KB package can pull in 3 MB of transitive dependencies. Always check the visualised bundle, not the dependency list.
Step 2: kill the asset bloat first
Assets are the easiest win and the place most teams have the most fat. We found:
- Four font families, each with six weights, each shipped as both TTF and OTF. Roughly 14 MB of fonts in an app that visibly used two weights.
- PNG illustrations exported at 3x for an app that only supported phone form factors, where 2x covers 95% of devices.
- A 6 MB onboarding video bundled into the binary instead of streamed.
Our rules now:
- Subset fonts to the glyphs you actually render. Tools like
glyphhangerorfonttoolscan reduce a 400 KB font to 60 KB. - Ship 2x PNG/JPEG, or better, use SVG for icons and illustrations.
react-native-svgis small and renders crisply at any density. - Never bundle video or audio over 1 MB. Host it on a CDN and download on first launch, with a fallback.
- Run every static image through
oxipng(PNG) ormozjpeg(JPEG) in your CI pipeline. The savings compound.
That alone reclaimed about 22 MB.
Step 3: audit native dependencies ruthlessly
The 58 MB of native libraries was the next target. The culprits, in order of impact:
- Two competing image libraries. The app had both
react-native-fast-imageandexpo-image. They each pull in their own native code. We standardised onexpo-imageand removed the other. - A barcode scanner used on exactly one screen. It contributed about 9 MB of native code because it bundled multiple ML model variants. We swapped to a lighter ZXing-based wrapper.
- Firebase modules we didn't use. The team had installed
@react-native-firebase/appplus Analytics, Crashlytics, Messaging, Performance, Remote Config, and Dynamic Links. They only actively used Crashlytics and Messaging. Removing the rest dropped about 6 MB. - Hermes vs JSC. Hermes was already enabled. If you're still on JSC in 2026, switch — Hermes alone usually saves several MB and improves cold start.
The general principle: every native module is a tax. Audit them quarterly. If a feature is used on one screen, ask whether a WebView, a server-rendered flow, or a lighter library can replace it.
ABI splits and the universal APK trap
If you're still shipping universal APKs, stop. The Android App Bundle splits native libraries per device ABI automatically. A user on arm64-v8a should never download armeabi-v7a or x86_64 binaries. Expo's EAS Build produces AABs by default — confirm yours does.
// app.json / app.config.js
{
"expo": {
"android": {
"buildType": "app-bundle"
}
}
}
On iOS, bitcode is gone (Apple deprecated it), so the equivalent lever is making sure you're not shipping x86_64 simulator slices in your release IPA. EAS handles this correctly; custom Fastlane setups sometimes don't.
Step 4: trim the JavaScript bundle
Nine megabytes of Hermes bytecode is not catastrophic, but it's worth a pass. The visualiser surfaced three patterns we see in almost every audit:
- Full lodash imports.
import _ from 'lodash'pulls the whole library. Uselodash-eswith named imports, or just write the three utility functions you actually need. - Moment.js. Still alive in legacy codebases. Replace with
date-fns(tree-shakable) ordayjs(tiny). We've never regretted this migration. - Icon libraries shipping every glyph.
react-native-vector-iconsbundles all icon sets unless you configure it. Most teams use 20 icons from one set. Either subset or switch to importing SVGs directly.
We also removed three animation libraries that had been added at different times for different screens. Reanimated 3 covers almost every case; the rest were dead weight.
Step 5: lock it down in CI
The worst part of size optimisation is that it regresses. Someone adds a dependency for a feature, ships it, and three months later you're back where you started. We now enforce a budget in CI.
# .github/workflows/size-check.yml (excerpt)
- name: Build release AAB
run: eas build --platform android --profile production --local
- name: Check size budget
run: |
SIZE=$(stat -c%s build.aab)
MAX=$((45 * 1024 * 1024))
if [ $SIZE -gt $MAX ]; then
echo "AAB exceeds 45 MB budget: $SIZE bytes"
exit 1
fi
It's crude, but it works. A PR that adds 8 MB now has to justify itself in review. We pair this with a weekly report that posts the bundle visualiser diff to Slack.
The numbers we landed on
After two sprints of work, the same app shipped at 38 MB on Android (base install, arm64) and 47 MB on iOS. Cold start on a mid-range Android device improved noticeably — we won't quote a precise figure because it varies by device, but the team felt it, and so did the analytics on time-to-interactive.
More importantly, install completion in the three markets we'd been worried about recovered to roughly where it had been a year earlier.
Where we'd start
If you're staring at an app that's grown out of control, do these three things this week, in order:
- Generate the bundle visualiser output and the unzipped IPA/AAB. Spend an hour just reading them. You'll find one or two libraries that don't belong.
- Subset your fonts and re-export images at 2x. This is unsexy work that produces the biggest single-day win.
- Add a size budget check to CI before you cut anything else, so your work doesn't decay.
If you'd rather not run this audit yourselves, it's the kind of engagement we do regularly — see our mobile development services for how we typically scope it. Either way: measure first, cut second, and budget forever.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading
Deep Linking in React Native 2026: Universal Links, App Links, and the Edge Cases That Bite
Deep linking looks trivial until a marketing campaign goes live and half your users land on the App Store instead of the screen you promised. Here's what actually works in 2026.

Expo Dev Client vs Expo Go in 2026: Stop Fighting Your Own Tooling
Expo Go is great until it isn't. Here's when to graduate to a custom dev client, what breaks during the switch, and how to keep your team productive while you migrate.

Background Tasks on React Native in 2026: What Actually Runs on iOS and Android
Background execution is the swampiest part of mobile. Here's what actually works on iOS and Android in 2026, what Expo gives you, and where you still need a native module.
