Optimize Your React Native App’s JavaScript Bundle

Hanna Sobolewska
Callstack Engineers
7 min readFeb 24, 2023

by Jakub Binda & Andrew Andilevko

The following article is part of The Ultimate Guide to React Native Optimization and explains how you can optimize your React Native app’s performance using different bundlers, like Re.Pack, `react-native-esbuild,` and `rnx-kit.`

Why is this important?

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. Sometimes, though, the final bundle becomes too large — and that’s where performance issues begin.

In other blog posts based on The Ultimate Guide to React Native Optimization, we touch on the following topics related to improving performance through understanding the React Native implementation details:

Be sure to check them out later. Now, let’s move on to the topic of optimizing your React Native app’s JavaScript bundle.

Using alternative bundlers to optimize performance

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 bundles like Webpack are the other way around. So when your project needs custom loaders, use symlinks or the proven Webpack configuration 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. In this article, we’ll take a closer look at Re.Pack, `react-native-esbuild,` and `rnx-kit` and consider their 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. It 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 remove 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.

The very first issue on Metro bundler’s GitHub is about symlink support. It remains open today, as there are various reasons Metro is blocked from introducing this functionality with the desired DX. There are ways to mitigate that particular shortcoming, but they require extra configuration. Webpack, on the other hand, as virtually any other bundler, and doesn’t have this issue. Re.Pack uses the Webpack bundler under the hood, so it provides symlinks functionality out-of-the-box, which could be invaluable from the developer experience point of view of some workflows like monorepos.

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. 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, 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.

On top of that, Webpack 5 introduced support for the concept of Module Federation. It’s a functionality that allows 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.

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. Also, the Fast Refresh functionality is limited compared to the Metro bundler. The Hot Module Replacement and React Refresh features require the full application to be reloaded with Webpack and Re.Pack, but they are supported by Metro.

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 greatly improves 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 could be a crucial point for some projects. And it does not have Fast Refresh or Hot Module Replacement, but this library supports live reload instead.

rnx-kit

Last but not least, we’d like to discuss Microsoft’s rnx-kit. It’s an interesting extension to Metro. `rnx-kit` is a package with a huge variety of scalable React Native development tools. It also has a custom bundler that works on top of the Metro bundler, enhancing it.

The pros of enhancing Metro with rnx-kit

As you already know, Metro does not support symlinks. `rnx-kit` provides the ability to fully work with symlinks. One more benefit compared to Metro is the tree shaking functionality out-of-the-box.

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 on 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.

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. If you need 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, plugins maintained by the community, and a lot of features like symlinks, aliases, etc. compared to other bundle tools.

If the Webpack ecosystem feels 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.‍

If you need help with performance, stability, user experience, or other complex issues, contact us! As React Native Core Contributors and active Open Source contributors, we will be happy to help.

This article was originally published at callstack.com on February 13, 2023.

--

--