This is an example project to build the common bundle file and the differential bundle file using Metro, and load the differential bundle asynchronously in app. Compare with loading the official bundle file synchronously, there was 20% ~ 25%(20 ~ 200 ms) decrease in the load time of react view by using loading the differential bundle asynchronously.
As we all known that there are three parts in a official bundle file: Polyfill、 Modules、 Requires. If you build two different official bundle files, you will find that there are many repeated content, which is close to 500K. In order to minimize the bundle file, we define a common bundle file, which only includes some basic modules(such as react
and react-native
). And we define a differential bundle file, which only includes your custom code.
Before React Native 0.55, we generally use google-diff-match-patch
or BSDiff
to build the differential bundle file, which needs the process of merging before your app loading the differential bundle file.
However, there is a new way to build the differential bundle file by using Metro.
- Modify the
common.js
like blew:
require("react-native");
require("react");
// Add other libs you want to add to the common bundle like this:
// require('other');
- Build the common bundle file with
--config metro.config.common.js
or use the command blew:
# For android:
npm run build_android_common_bundle
# For iOS:
npm run build_ios_common_bundle
- Build the differential bundle file with
--config metro.config.diff.js
or use the command blew:
# For android:
npm run build_android_index_diff_bundle
# For iOS:
npm run build_ios_index_diff_bundle
- Copy all output files to the dir of app project or use the command blew:
npm run copy_files_to_projects
- Run the app project by Android Studio or XCode.
NOTICE: There are two ways to start an activity with react native in android app: one as SYNC, the other as ASYNC. It is same with the official reference implementation when using SYNC. As for ASYNC, it will start a general Activity (or ViewController), which will load a common bundle file, after that it will start a custom Activity (or ViewController) using react native, which will ONLY load the differential bundle file. The load time of react view will display by log and toast.
Android File | Size | Size After gzip |
---|---|---|
common.android.bundle | 637.0 K | 175K |
index.android.bundle (Original) | 645.0 K | 177K |
diff.android.bundle (Using Metro) | 8.3 K | 2.5 K |
diff.android.bundle (Using BSDiff) | 3.9 K | 3.9 K |
diff.android.bundle (Using google-diff-match-patch) | 11.0 K | 3.0 K |
iOS File | Size | Size After gzip |
---|---|---|
common.ios.bundle | 629.0 K | 173K |
index.ios.bundle (Original) | 637.0 K | 176K |
diff.ios.bundle (Using Metro) | 8.3 K | 2.5 K |
diff.ios.bundle (Using BSDiff) | 3.9 K | 3.9 K |
diff.ios.bundle (Using google-diff-match-patch) | 11.0 K | 3.0 K |
You can find more information about
google-diff-match-patch
andBSDiff
by visiting this.
Loading Type | Redmi 3 | Huawei P20 | iPhone 6s | iPhone XS MAX |
---|---|---|---|---|
Synchronization | 868.2 ms | 337.8 ms | 405.3 ms | 109.2 ms |
Asynchronization | 643.4 ms | 253.2 ms | 300.2 ms | 88.3 ms |
-25.89% | -25.04% | -25.88% | -18.68% |
The key to build a differential bundle file is making the id of input module invariant during the process of bundling. It is noteworthy that the Metro provides two configuration items in metro.config.js
file: createModuleIdFactory(path)
and processModuleFilter(module)
.
By customizing createModuleIdFactory(path)
, we used the hash of the file as the key to allocate module id.
// See more code int metro.config.base.js
// ...
function getFindKey(path) {
let md5 = crypto.createHash("md5");
md5.update(path);
let findKey = md5.digest("hex");
return findKey;
}
// ...
In order to avoid duplication of allocation of module id, we use a local file (repo_for_module_id.json
) to store the result of allocation during the process of building.
"8b055b854fd2345d343b6618c9b71f7e":
{
"id": 5,
"type": "common"
}
By customizing processModuleFilter(module)
, we compare the hash of input module
with local-storage. If input module
is included by common bundle file, it will be filtered and will not be written to the output bundle file.
// See more code int metro.config.base.js
// ...
buildProcessModuleFilter = function(buildConfig) {
return moduleObj => {
let path = moduleObj.path;
if (!fs.existsSync(path)) {
return true;
}
if (buildConfig.type == BUILD_TYPE_DIFF) {
let findKey = getFindKey(path);
let storeObj = moduleIdsJsonObj[findKey];
if (storeObj != null && storeObj.type == BUILD_TYPE_COMMON) {
return false;
}
return true;
}
return true;
};
};
// ...
However, the polyfills is also written in the output bundle file after running Metro, we should remove those code by ourselves.
For example, we made a script file call removePolyfill.js
in the dir __async_load_shell__
, you can use it by run:
node ./__async_load_shell__/removePolyfill.js {your_different_bundle_file_path}
Because the common bundle file includes all basic codes, we should make sure a good timing to load the common bundle file before loading the differential bundle file.
In the demo app, a guide activity is created to load the common bundle file, which is also used to simulate a PARENT activity of the activity using react native. Sometimes, The guide activity can also usually be displayed the entrance of your business which was builded by react native in your official app.
All related code was organized in package com.marcus.rn.async
. There are some key points about the implementation:
- We use the
ReactNativeHost
to point the path of common bundle file, and callcreateReactContextInBackground()
to initialize the context of React Native and load the common bundle file. - In order to get approximate finish time of loading common bundle file, we use
addReactInstanceEventListener()
ofReactInstanceManager
to add custom listener and monitor the eventonReactContextInitialized
to indicate the finish of loading common bundle file. - We redefine
ReactActivityDelegate
class to suit the scene of loading asynchronously. which can be found by name withAsyncLoadActivityDelegate.java
. - Because the guide activity and the container activity of react native MUST shared the same
AsyncLoadActivityDelegate
object, we build a singleton class calledAsyncLoadManager
to provider the object. - The load time of react view will be displayed by log and toast, which records time period from
onCreate()
of the activity to monitor the event calledCONTENT_APPEARED
. - As for the global variable problem in javascript, we should clear the context of react native before reused it. we provides a simple way to fix the problem by rebuild the
AsyncLoadActivityDelegate
object, you can see the code inprepareReactNativeEnv()
inAsyncLoadManager
.
Because the common bundle file includes all basic codes, we should make sure a good timing to load the common bundle file before loading the differential bundle file.
In the demo app, a guide view-controller is created to load the common bundle file, which is also used to simulate a PARENT view-controller of the view-controller using react native. Sometimes, The guide view-controller can also usually be displayed the entrance of your business which was builded by react native in your official app.
There are some key points about the implementation:
- We expose the
executeSourceCode
inRCTBridge
like this:
#import <Foundation/Foundation.h>
@interface RCTBridge (RnLoadJS)
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;
@end
- We use the
sourceURLForBridge
ofRCTBridgeDelegate
to point the path of common bundle file, and call[[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]
to initialize the context of React Native and load the common bundle file. - Because the guide view-controller and the container view-controller of react native MUST shared the same
RCTBridge
object, we build a singleton class calledMMAsyncLoadManager
to manage the object. - The load time of react view will be displayed by log and toast, which records time period from
viewDidLoad
of the view-controller to monitor the notification calledRCTContentDidAppearNotification
. - As for the global variable problem in javascript, we should clear the context of react native before reused it. we provides a simple way to fix the problem by rebuild the
RCTBridge
object, you can see the code inprepareReactNativeEnv
method inMMAsyncLoadManager
.
PRs accepted.