Loading multiple bundles in react-native | Code splitting using Metro
- 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.
- 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.
- 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.
- New JS bundle size = 700 KB (common.bundle) + 100 KB (business1.bundle) + 100 KB (business2.bundle) + 100 KB (business3.bundle) = 1000 KB
- Pre-load this common code when app is still in native flow.
- 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-
- Split react-native single bundle into common + business bundles.
common.android.bundlewill contain only React & react-native libraries whereas
business.android.bundlewill contain only business JS files.
common.android.bundlewhile app is still in native flow i.e. in MainActivity
- On-demand load
Introduction to Metro
Metro has three separate stages in its bundling process:
- Resolution: Stage where file/module resolution is done to build dependency graph
- Transformation: Transpilation stage
- Serialization: Stage where all the transpiled modules are combined to generate one or multiple bundles
Code splitting using Metro
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.
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.
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.
Now create metro.business.config.js with below content-
Here we are doing three things-
- Generate module ID for business files: Instead of starting from 0, now we are starting from last generated ID+ 1 of common bundle
- Filtering modules which are already part of common bundle i.e. React & react-native libraries: We are using information available in fileToIdMap.txt file
- 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.
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-
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
To calculate the startup time for both the flows, I printed timestamps at two places and then took the difference.
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.
⚡️ React Native startup speed optimization-JS article [The most complete network, worthy of…
If you like my article, I hope to like it 👍 Collection 📁 Comment 💬 Three consecutive support, thank you, this is…