Mobile — App Performance Optimization
Reducing React Native Bundle Size by 40%
Direct answer
Cutting a React Native JS bundle by forty percent is usually achievable through four levers: analyzing the bundle with source maps to see what is actually inside, eliminating barrel imports that drag entire libraries in, replacing one or two heavyweight dependencies with lighter equivalents, and enabling tree shaking plus native-side shrinking like R8. In my experience the first analysis almost always reveals that a handful of dependencies account for a disproportionate share of the bundle — you fix the treemap's biggest rectangles, not a hundred small ones.
Bundle size is startup time in disguise: every kilobyte of JavaScript must be loaded and initialized before your first screen is interactive. This is the sequence I follow when a client asks me to put their bundle on a diet, in the order that produces the largest cuts first.
Key facts, with sources
- The median crash-free session rate across mobile apps in 2025 was 99.95%, with top-performing teams at the 75th percentile holding 99.99%. (Luciq Mobile App Stability Outlook 2025)
- Compared with JavaScriptCore, the Hermes engine cuts typical React Native app startup from about 4.5 seconds to about 2.0 seconds, memory from about 185 MB to 136 MB, and engine app-size overhead from about 12 MB to 8 MB. (OneUptime Blog)
- Real-world production migrations to React Native's New Architecture report around 43% faster cold starts, 39% faster rendering, and 26% lower memory usage. (RapidNative)
- Shopify's performance bar for its React Native apps is critical screens loading in under 500 milliseconds at the 75th percentile, alongside over 99.9% crash-free sessions. (Shopify Engineering)
- Published optimization case studies include Discord cutting app startup time in half and Coinbase reporting an 80% funnel performance improvement after its React Native rewrite. (CatDoes)
Start with the treemap, not with guesses
Every bundle-size engagement I run starts the same way: produce a release bundle with source maps and feed it to a source map explorer. The treemap answers the only question that matters — where the bytes actually are. Teams consistently guess wrong; the dependency they suspect is often trivial while an innocuous-looking utility library or a locale data file dominates.
Record the exact byte count of this first bundle. Every change you make afterward gets validated against it by re-running the same command. Bundle work without a before-and-after number degenerates into cargo-culting import styles that may do nothing on your particular Metro configuration.
npx react-native bundle \
--entry-file index.js \
--platform android \
--dev false --minify true \
--bundle-output build/index.android.bundle \
--sourcemap-output build/index.android.bundle.map
npx source-map-explorer build/index.android.bundle --no-border-checks
ls -lh build/index.android.bundle # record this baselineKill barrel imports first
Barrel files — index files that re-export everything from a directory or package — are the single most common cause of bloat I find. Import one function through a barrel and Metro follows every re-export, pulling in modules you never call, along with their transitive dependencies. Utility libraries and icon packs are the classic offenders, but internal barrels are just as guilty: a components index that re-exports forty components means importing a Button loads all forty.
The fix is boring and effective: import from the concrete file path. For your own code, either delete internal barrels on hot paths or keep them for developer ergonomics in places where everything genuinely gets used together.
// Before — the barrel pulls in the entire library
import { debounce } from 'lodash';
// After — one module, one function
import debounce from 'lodash/debounce';
// Same principle for your own code:
// import { Button } from '@app/components'; // loads the whole index
import { Button } from '@app/components/Button'; // loads one fileReplace the heavyweight dependencies
The treemap usually shows two or three libraries that are wildly oversized for the job they do. Date libraries that bundle every locale, a full charting suite used for one sparkline, a validation library where a few hand-written checks would do, or a legacy HTTP client duplicating what fetch provides. Each replacement is a small, testable PR: swap the API surface, run the affected screens, re-measure.
I apply a rule before adding any new dependency: check its cost in the bundle before merging, not after. A package that saves an afternoon of coding but permanently taxes every cold start for every user is frequently a bad trade — and it is much easier to reject at review time than to excise a year later when a dozen call sites depend on it.
Turn on tree shaking and dead code elimination
Metro historically did not tree-shake, which is why the barrel problem bites so hard — but the ecosystem has moved. Newer Expo SDK toolchains offer experimental tree shaking that drops unused exports at build time, and alternative bundlers support code splitting for very large apps. If you are on a current toolchain, enabling this is often free size reduction; verify with the treemap because configuration nuances can quietly disable it.
Also sweep for self-inflicted dead code: feature-flagged experiments that ended months ago, A/B variants that lost, entire screens no route can reach, and development-only utilities that ship in release because nothing strips them. I typically find at least a few of these in any codebase older than a year.
Do not forget the native side of app size
The JS bundle is one component of what users download. On Android, enable R8 shrinking so unused native and Java or Kotlin code is stripped, and ship App Bundles so each device downloads only its own ABI and density resources. On iOS, put images in asset catalogs so thinning delivers only the variants a device needs, and audit which architectures and resources your third-party pods drag in.
Assets deserve their own pass: uncompressed PNGs that should be WebP, bundled fonts with three used glyph weights out of nine shipped, video onboarding files embedded rather than streamed. When a client asks for a forty percent reduction in download size, asset work often contributes as much as JavaScript work.
Lock the gains in with a CI budget
Bundle size regresses the way it grew: one innocent dependency at a time. After a reduction effort, I add a CI step that builds the release bundle, compares its size to a committed baseline, and fails the pipeline past a small tolerance. The failing check forces the conversation — is this new dependency worth its cost — at the moment it is cheapest to have, before anything ships.
Pair the hard gate with visibility: post the size delta on every pull request so engineers see the consequence of each import without having to go looking. Teams that adopt both mechanisms tend to hold their gains; teams that rely on memory and good intentions are usually back where they started within two or three quarters.
When to hire senior help
Bring in a senior performance specialist when your crash-free rate sits below roughly 99.9%, startup exceeds a couple of seconds on mid-range Android, or the team lacks profiling experience with Hermes, the JS thread, and native tooling. Performance rescue work is diagnostic in nature, so a short expert engagement typically finds the handful of hot spots that in-house teams spend months guessing at. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Profiling only on high-end iPhones in dev mode, then discovering the real user base on low-end Android devices experiences multi-second startups and dropped frames
- ✕Rendering long feeds with ScrollView or an untuned FlatList instead of a virtualized list like FlashList, blocking the JS thread during scroll
- ✕Passing new inline functions and object literals on every render without memoization, causing cascading re-renders that never show up until lists grow
- ✕Ignoring JS bundle size and startup path, shipping no lazy loading or inline requires, so time-to-interactive balloons as the app grows
Frequently asked questions
How do I check my React Native bundle size?
Build a release bundle locally with the react-native bundle command, passing dev false and minify true along with a source map output, then inspect it with a source-map-based analyzer to see a treemap of which modules and dependencies occupy the bytes. Check the file size directly as your baseline number, and re-run the identical command after each optimization to validate the change actually helped.
What makes React Native bundles so large?
The usual culprits are barrel imports that pull entire libraries in when you use one function, heavyweight dependencies like full date, chart, or utility suites doing small jobs, locale and polyfill data that modern engines make redundant, dead feature-flag code, and the historical lack of tree shaking in Metro. A source map treemap typically shows a few large offenders rather than uniform bloat.
Does reducing bundle size improve React Native startup time?
Yes, directly. Every module in the bundle must be loaded — and unless lazily required, initialized — before your app becomes interactive, so a smaller bundle means less work on every cold start. The effect is largest on low-end Android devices. Combine size reduction with Hermes bytecode and inline requires so the remaining code also loads lazily, and measure cold start before and after.
Which performance metrics should we actually track?
Crash-free session rate (the 2025 median is 99.95%, and below 99.8% is a red flag), cold start time, time-to-interactive, P75 screen-load time, and frame rate during scrolling. These need real-user monitoring in production, not just lab measurements.
Why is our React Native app slow on Android but fine on iOS?
The usual causes are testing only on flagship devices, JS-thread-blocking work, unvirtualized lists, and running an old React Native architecture or the JSC engine. Hermes and New Architecture migrations show measured gains of roughly 26 to 43% on memory and cold start, so upgrading the foundation is often the highest-leverage fix.
Does performance really affect revenue or is it an engineering vanity metric?
It compounds directly into acquisition and retention: crashes and slowness drive uninstalls and one-star reviews, and lower ratings measurably cut store conversion rates. That is why top consumer apps hold themselves to 99.99% crash-free sessions and sub-500ms screen loads.
Bottom line: Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.