Optimize Your React Native App’s JavaScript Bundle

Authors
Jakub Binda
Software Engineer
@
Callstack

React Native application’s logic is mostly located in the JavaScript code which runs in the JavaScript engine (JavaScriptCore or Hermes). But before loading JavaScript code into the app, it should be bundled, usually into a single JS file or sometimes to multiple files. React Native provides a built-in tool for JavaScript code bundling called Metro

Using alternative bundlers to optimize performance

Like any bundler, Metro takes in an entry file and various options and gives you back a single JavaScript file that includes all your code and its dependencies, also known as a JavaScript bundle. According to official docs, Metro speeds up builds using a local cache of transformed modules out-of-the-box. 

Metro trades configurability for performance, whereas other bundlers like Webpack are the other way around. So when your project needs custom loaders or the extensive Webpack configurability for bundling JavaScript code and splitting app logic, there are a few alternative bundlers that could be used in React Native apps and provide more configuration features. Each of them has some benefits and limitations.

Re.Pack

Re.Pack is a Webpack-based toolkit to build your React Native application with full support of the Webpack ecosystem of loaders, plugins, and support for various features like symlinks, aliases, code splitting, etc. Re.Pack is the successor to Haul, which served a similar purpose but balanced a different set of tradeoffs and developer experience. By the way, we’re proud to say that Re.Pack it brought to you by our React Native development company – and we put in every effort to make it as valuable and developer-friendly as possible. 

Benefits of using Re-Pack in your React Native project

The ecosystem part of Webpack is crucial for many developers, since it’s the most popular bundler on the web, making the community behind loaders and plugins its key advantage. Thanks to that pluggability, it provides ways to improve the build process and Webpack’s overall performance. At least for the parts that are not connected to the internal module graph building and processing. Such parts would be, e.g., JavaScript and TypeScript transpilation or code minification. You can replace Babel transpiler and Terser minifier with faster alternatives like ESBuild thanks to the esbuild-loader or swc with swc-loader.

Another Webpack feature that helps our apps achieve better performance is reducing the amount of code in the final bundle with tree shaking. Tree shaking is a dead code elimination technique done by analyzing the import and export statements in the source code and determining which code is actually used by the application. Webpack will then remove any unused code from the final bundle, resulting in a smaller and more efficient application. The code that’s eligible for tree shaking needs to be written in ECMAScript modules (import and export statements) and mark itself as side-effect free through package.json sideEffects: false clause.

Webpack has support for symlinks but since React Native 0.72, Metro offers that as well in an experimental form. And since v0.73 it’s turned on by default. Symlinks prove useful inside of monorepos, where node modules can be optimally shared between different workspaces.

Re.Pack also offers the ability to use asynchronous chunks to split your bundle into multiple files and load them on-demand, which can improve initial loading times if you’re using the JavaScriptCore engine. However, it won’t provide that much value when used with Hermes, which leverages the memory mapping technique for dynamic reading only the necessary parts of the bundle’s bytecode directly from the RAM. It might make a slight difference when your app’s bundle is really big, and you are targeting low-end Android devices. But there’s a twist to that! Webpack doesn’t really care whether you load the dynamic chunk from the filesystem or remote. Hence, it allows for dynamic loading code that’s never been there in the app bundle in the first place - either directly from a remote server or a CDN. Now, this can help you with reducing not only the initial load time, but also the precious app size. It also opens up a way to Over-The-Air (OTA) updates that target only a small part of your app.

On top of that, Webpack 5 introduced support for the concept of Module Federation. It’s a functionality that allows for code-splitting and sharing the split code parts (or chunks) between independent applications.

It also helps distributed and independent teams to ship large applications faster. Giving them the freedom to choose any UI framework they like and deploy independently while still sharing the same build infrastructure. Re.Pack 3 supports this functionality out-of-the-box and provides you with a lot of utilities that prove useful in such scenarios e.g. CodeSigningPlugin can help you with integrity verification of remotely loaded bundles.

Where Metro works out better than Re.Pack

All these configurations and flexibility affect the build process. The build speed is a little bit longer than the default Metro bundler due to customization options. When switching from Metro, it might require you to solve some resolution errors, as the algorithms differ between the two bundlers. Also, the Fast Refresh functionality is limited compared to the Metro bundler. The Hot Module Replacement (HMR) and React Refresh features might sometimes require the full application reload with Webpack and Re.Pack. When working with Module Federation, the HMR functionality is also limited to refreshing parts of the app originating from the host. For the remote modules a full reload is required.

If you don’t need the huge customization that the Webpack ecosystem offers or don’t plan to split your app code, then you may as well keep the default Metro bundler.

react-native-esbuild

The next bundler that can help you optimize your React Native app’s performance is react-native-esbuild, which comes with speed, tree shaking, compatibility, and configurability. Let’s take a closer look at this tool.

Why it's worth considering react-native-esbuild for your React Native project

One of the main benefits of react-native-esbuild is fast builds. It uses the ESBuild bundler under the hood which has huge improvements in bundling performance even without caching. It also provides some features like tree shaking and is much more configurable compared to the Metro bundler. 

ESBuild has its own ecosystem with plugins, custom transformers, and env variables. This loader is enabled by default for .ts, .tsx, .mts, and .cts files, which means ESBuild has built-in support for parsing TypeScript syntax and discarding the type annotations. However, ESBuild does not do any type checking, so you will still need to run type check in parallel with ESBuild to check types. This is not something ESBuild does itself.

The drawbacks of react-native-esbuild

Unfortunately, react-native-esbuild has some tradeoffs, so it is very important to select the right bundler by paying attention to them as well. It doesn’t support Hermes, which is now the default engine for React Native. And it does not have Fast Refresh or Hot Module Replacement, but this library supports live reload instead.

rnx-kit

Last but not least, Microsoft's rnx-kit is an interesting extension to Metro. It is a package with a huge variety of React Native development tools. Historically, it enabled the use of symlinks with Metro, before it was officially supported.

The pros of enhancing Metro with rnx-kit

Another benefit compared to Metro is the tree shaking functionality out-of-the-box, through the use of ESBuild for bundling. 

Metro supports TypeScript source files, but it only transpiles them to JavaScript. Metro does not do any type-checking. rnx-kit solves this problem. Through the configuration, you can enable type-checking. Warnings and errors from TypeScript appear in the console.

Also, rnx-kit provides duplicate dependencies and cyclic dependencies detection out-of-the-box. This could be very useful in reducing the size of the bundle which leads to better performance and prevents cyclic dependencies issues. Note that you will have to solve these issues yourself, but thankfully rnx-kit documentation provides some insights on how to deal with them.

Ship less JavaScript to users and save devs’ time when bundling

The choice of a bundle tool depends on the specific case. It is impossible to select only one bundler for all the apps.

As you can see, tree-shaking in React Native can be achieved through use of Webpack (via Re.Pack) or ESBuild (via rnx-kit or react-native-esbuild). Tree-shaking implementation differs between bundlers, so it might be feasible to check the results of both and determine what's best for your app. Note that tree-shaking through rnx-kit is still in beta, but the results are optimistic so far. It’s reasonable to expect the bundle size difference between 0% and 20%, and in rare cases, even more than that.

Should you feel a need for customization options provided by the Webpack ecosystem or plan to split your app code, then we would suggest using Re.Pack for its widely customizable configuration, a huge amount of loaders, and plugins maintained by the community. 

If the Webpack ecosystem feels like an overhead, then it is better to stay with the default Metro bundler or try to use other bundler options like react-native-esbuild and rnx-kit, which also provides some benefits like decreased build time, using esbuild under the hood, symlinks, and typescript support out-of-the-box. But be careful and always pay attention to the tradeoffs that come with a new bundling system.

Insights

Learn more about Performance

cover

Jul 3Podcast

Why Nitro Is More Than a Faster Bridge

Marc Rousavy (Margelo) talks Nitro modules, native state, zero-copy data, Vision Camera, and why speed is only part of the story.

cover

May 25Article

Profile React Components in React Native Release Builds

Learn how to profile React components in React Native release builds with @callstack/inspector, using an Expo app setup that requires zero native code changes.

cover

Nov 21Article

Memory Ownership Models: When JavaScript Meets Native Code

A deep dive into how React Native handles memory when JavaScript and native code share ArrayBuffer data. Learn why async processing can break, how GC affects raw pointers, and the safe patterns for building high-performance native integrations.

cover

Oct 14Article

From Files to Buffers: Building Real-Time Audio Pipelines in React Native

Learn how to move beyond file-based audio APIs and work directly with in-memory PCM data in React Native. This guide shows how to construct WAV buffers, use JSI for zero-copy transfers, and enable real-time audio processing.

cover

Sep 8Article

How to Add Custom JSI Bindings in Your TurboModule (and Why You Might Need Them)

TurboModules provide a type-safe way to write native code in React Native. JSI sits underneath as a lower-level API, enabling direct interaction with the JavaScript runtime. Beyond what TurboModules expose, JSI gives you access to powerful features, such as working directly with ArrayBuffers. This article shows how to inject a custom binding step by step.

cover

Jul 24Article

Exploring Fibers: Cooperative Multitasking and Lock-Free Task Execution

Explore how fibers enable lightweight, cooperative multitasking. This guide walks through building a simple scheduler, managing task dependencies without locks, and using Tracy to visualize performance.

cover

May 21Article

Multithreading Isn’t Free: Performance Pitfalls Visualized

Learn how to profile and optimize multithreaded apps. Discover why more threads don’t always mean faster results and how bad thread contention really is.

Event Cover

Apr 28Webinar

Master JavaScript Performance in React Native (Part 2)

Learn to fix JS performance issues in React Native-from debounce bugs to Redux selectors, in this hands-on webinar with real code examples.

Events Cover

Apr 24Talk

Tips & Tricks From The Ultimate Guide to React Native Optimization 2025

View flattening, React 19 hooks, real DevTools tips-learn what actually helps your React Native app run faster.

Events Cover

Apr 24Talk

User-Focused Performance Tracking

Discover how Expensify defines and tracks performance metrics that actually impact user experience.

cover

Dec 18Podcast

React Native FastIO: Nitro-Powered Performance Boost

Listen to Mike Grabowski talk about React Native FastIO, a collection of primitives for working with data in React Native and its performance benefits.

cover

Oct 15Podcast

Superfast Nitro Modules With Marc Rousavy

Even faster React Native apps? That's possible with Nitro Modules! Tune in to the conversation with their creator, Marc Rousavy.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.