Loading multiple bundles in react-native | Code splitting using Metro

Varun Kumar
7 min readJun 9, 2021

React native is awesome, there is no doubt about this and with the active community support, it’s getting matured day by day. If you are a Javascript guy and want to try native app development, welcome to the party.

Despite all the awesomeness, there are still few things that bother developers e.g. huge apk size, significant startup time etc. A hello world android react-native app will be roughly of 7 MB in size when distributed via Android app bundle. This is fine for smaller apps but for bigger apps, a bloated apk is going to cause trouble. Most of the bigger apps in industry use a hybrid approach i.e. their initial flow is in Android native and they switch to react-native for some of their modules / line of business. In this post I am going to talk about code splitting and pre-loading of Javascript bundle in Android and how can it optimize size and startup time of a hybrid react-native app.

Objective

  1. Consider a scenario where an app has three React Activities for 3 different businesses. Each React Activity when invoked, load its own react-native app(module) to it.
  2. Let’s assume that JS bundle size of each module is 800 KB out of which library (react & react-native) size is 700 KB. Total JS bundle size = 2400 KB.
  3. Since Each JS bundle will contain transpiled React & react-native libraries, so let’s extract this 700 KB common code and put in a separate bundle.
  4. New JS bundle size = 700 KB (common.bundle) + 100 KB (business1.bundle) + 100 KB (business2.bundle) + 100 KB (business3.bundle) = 1000 KB
  5. Pre-load this common code when app is still in native flow.
  6. Save app size as well as react-native startup time with this process.

Proof of Concept

To demonstrate pre-loading & code splitting, I have created a POC app in which I am starting two react-native apps, one with complete bundle (what we do normally) and another with common + business bundle. I am then comparing startup time for both RN apps. Complete source code of this POC is available at https://github.com/varunon9/react-native-multiple-bundle

This demo app has following flow-

Tasks breakdown

  1. Split react-native single bundle into common + business bundles. common.android.bundle will contain only React & react-native libraries whereas business.android.bundle will contain only business JS files.
  2. Pre-load common.android.bundle while app is still in native flow i.e. in MainActivity
  3. On-demand load business.android.bundle from ReactActivity

Introduction to Metro

Metro is a JavaScript bundler. It takes in an entry file and various options, and gives us back a single JavaScript file that includes all our code and its dependencies.

Metro has three separate stages in its bundling process:

  1. Resolution: Stage where file/module resolution is done to build dependency graph
  2. Transformation: Transpilation stage
  3. Serialization: Stage where all the transpiled modules are combined to generate one or multiple bundles
Structure of metro config file, source Metro Docs

Code splitting using Metro

React native uses Metro for bundling Javascript files. As of now there is no official way to generate multiple bundles but this can be achieved using custom metro config files. Out of the three stages of bundling process, we are mainly interested in Serializer stage.

We want to divide out Javascript bundles into two parts — common (react & react-native) + business (business JS files + 3rd party libraries). Let’ go with following steps-

common.js file

Create a common.js file with below contents-

This file has only dependency on React & react-native so our common bundle will only contain transpiled code of these two libraries.

metro.common.config.js file

Create metro.common.config.js with below contents-

Here we are generating module ID for each of the modules starting from 0. This is default ID generation method used in Metro. We are also persisting this info in fileToIdMap.txt file so we can later consume it while generating business bundle.

Now we can use below command to generate common.android.bundle and put in assets directory of base app-

You will notice that fileToIdMap.txt has content something like this-

This means that our entry file common.js has been assigned module ID 0 and so on.

business.js file

Create a business.js file with below contents-

This will be the starting point for our app. It’s same as default index.js but we are calling it as business.js for better terminology.

metro.business.config.js file

Now create metro.business.config.js with below content-

Here we are doing three things-

  1. Generate module ID for business files: Instead of starting from 0, now we are starting from last generated ID+ 1 of common bundle
  2. Filtering modules which are already part of common bundle i.e. React & react-native libraries: We are using information available in fileToIdMap.txt file
  3. Not generating any polyfill functions since those are already available in common bundle. However require.js polyfill function would still be generated since that is hardcoded in Metro source code

Now we can use below command to generate business.android.bundle and put in assets directory of base app-

Removing require.js polyfill function

As a last step, we need to remove require.js polyfill function from business bundle.
require.js polyfill function initializes a variable modules and use it to cache resolved modules. Since this is already part of common.android.bundle, we don’t want to override it from business.android.bundle. In a minified business bundle, this will be the first line so we can remove it manually or can automate it via npm scripts. Failing to remove this line will cause error com.facebook.jni.CppException: Requiring unknown module.

Pre-loading common bundle

To pre-load common.android.bundle, we can create a Singleton ReactInstanceManager class and load the bundle from native flow, in this case MainActivity.

This will be invoked from MainActivity.java

SingletonReactInstanceManager class will be something like this-

On-demand loading business bundle

Now since we already have loaded common bundle in memory, all we have to do now is load business bundle and start the React Native app flow. We can do so by reusing same SingletonReactInstanceManager class-

End result

This is how demo app looks like. As soon as app gets opened, I pre-load common bundle and on click of Multi Bundle Rn App, I load business bundle and render React app in RootView. To compare the startup time I am also loading another react-native app on click of Single Bundle Rn App. Here I am using full bundle index.android.bundle that is generated by default config file metro.config.js

Startup time observation

startup time for both the flow in milliseconds

To calculate the startup time for both the flows, I printed timestamps at two places and then took the difference.

MainActivity.java

Timestamp from Native flow

App.js

Timestamp from react-native flow

Conclusion

We can observe that code-splitting & pre-loading is not only saving app size but also reducing initial startup time of react-native flow. Here we generated plain JS bundle but using hermes command we can generate bytecode output as well for better performance. This is just a POC and in production, we might run into some issues.
Please refer https://github.com/varunon9/react-native-multiple-bundle for complete source code. Do let me know your thoughts in comments 😊

Thank you Shashwat for mentorship & guidance.

References

--

--