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 <rte-code>package.json<rte-code> <rte-code>sideEffects: false<rte-code> 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. <rte-code>CodeSigningPlugin<rte-code> 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 <rte-code>.ts<rte-code>, <rte-code>.tsx<rte-code>, <rte-code>.mts<rte-code>, and <rte-code>.cts<rte-code> 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, <rte-code>react-native-esbuild<rte-code> 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. <rte-code>rnx-kit<rte-code> solves this problem. Through the configuration, you can enable type-checking. Warnings and errors from TypeScript appear in the console.
Also, <rte-code>rnx-kit<rte-code> 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 <rte-code>rnx-kit<rte-code> or <rte-code>react-native-esbuild<rte-code>). 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 <rte-code>rnx-kit<rte-code> 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 <rte-code>react-native-esbuild<rte-code> and <rte-code>rnx-kit<rte-code>, 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.