React Native startup speed optimization starts from Native convenience

Posted by Rincewind on Wed, 09 Mar 2022 12:44:51 +0100

There is a classic question in Web Development: "what happens in the browser from entering URL to page rendering?"

According to my research, this problem has a history of at least ten years. In the ever-changing front-end circle, this question can be asked all the time because it is a very good question, involving a lot of knowledge points. When doing some performance optimization, you can start from this problem, analyze the performance bottleneck, and then apply the right medicine to the case for optimization.

However, today we will not talk about the performance optimization of the Web. We just use the analysis idea of the classic problem just now to analyze the startup performance optimization of React Native one by one from the startup of React Native to the completion of the first rendering of the page, combined with the source code of React Native and the new architecture of 1.0.

If you like my article, I hope you like it 👍 Collection 📁 Commentary 💬 Thank you for your support. It's really important to me!

Reading reminder: 1. The source code content in the article is RN version 0.64 2. The content of source code analysis involves four languages: Objective-C, Java, C + + and JavaScript. I try to make it easy to understand. If I really don't understand it, I can read the conclusion directly

0.React Native start process

As a Web front-end friendly hybrid development framework, React Native can be roughly divided into two parts at startup:

  • Operation of Native container
  • Operation of JavaScript code

The Native container startup is in the existing architecture (version number less than 1.0.0): it can be roughly divided into three parts:

  • Native container initialization
  • Full binding of Native Modules
  • Initialization of JSEngine

After the container is initialized, the stage is handed over to JavaScript, and the process can be divided into two parts:

  • Loading, parsing and execution of JavaScript code
  • Construction of JS Component

Finally, JS Thread sends the calculated layout information to the Native end to calculate the Shadow Tree. Finally, UI Thread performs layout and rendering.

For the performance optimization of the rendering part, see what I wrote before React Native performance optimization guide , I introduced the common routines of RN rendering optimization from the aspects of rendering, pictures, animation and long list. Interested readers can go to check them. I won't introduce them here.

Tip: during React Native initialization, multiple tasks may be executed in parallel, so the figure above can only show the general process of React Native initialization, and does not correspond to the execution timing of the actual code one by one.

1. Upgrade React Native

The best way to improve the performance of React Native applications is to upgrade the large version of RN once and for all. After our application was upgraded from 0.59 to 0.62, our APP did not do any performance optimization, and the startup time was directly shortened by 1 / 2. When the new architecture of React Native is released, the startup speed and rendering speed will be greatly improved.

Of course, the version upgrade of RN is not easy (across IOS, Android and JS, compatible with destructive updates). I wrote an article before React Native upgrade Guide (0.59 - > 0.62) Article, if there is upgrading ideas of the old fellow can read for reference.

2.Native container initialization

The initialization of the container must start from the entry file of the APP. Next, I will select some key codes and sort out the initialization process.

iOS source code analysis


AppDelegate.m is the entry file of iOS. The code is very concise. The main contents are as follows:

// AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // 1. Initialize an RCTBridge to implement the method of loading jsbundle s
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

  // 2. Use RCTBridge to initialize an RCTRootView
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge

  // 3. Initialize UIViewController
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  // 4. Assign RCTRootView to the view of UIViewController
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;

In general, the entry file does three things:

  • Initialize an RCTBridge to implement the method of loading jsbundle s
  • Use RCTBridge to initialize an RCTRootView
  • Assign RCTRootView to the view of UIViewController to mount the UI

From the entry source code, we can find that all initialization work points to RCTRootView, so let's see what RCTRootView does next.


Let's take a look at the header file of RCTRootView first. We'll just look at some methods we focus on:

// RCTRootView.h

@interface RCTRootView : UIView

// AppDelegate. Initialization method used in M
- (instancetype)initWithBridge:(RCTBridge *)bridge
                    moduleName:(NSString *)moduleName
             initialProperties:(nullable NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;

From the beginning of the document:

  • RCTRootView inherits from UIView, so it is essentially a UI component;
  • When RCTRootView calls initWithBridge, an initialized RCTBridge is passed in

In rctrootview M file, initWithBridge will listen to a series of JS loading listening functions during initialization. After listening to the loading of JS Bundle file, it will call appregistry in JS Runapplication(), start RN application.

After analyzing here, we found that rctrootview M only realizes the monitoring of various events of RCTBridge, which is not the core of initialization, so we have to go to the file of RCTBridge.


RCTBridge. In M, the initial invocation path is a bit long, and the source code is long. In sum, the last call is (void)setUp, and the core code is as follows:

- (Class)bridgeClass
  return [RCTCxxBridge class];

- (void)setUp {
  // Get bridgeClass. The default is rctcxbridge
  Class bridgeClass = self.bridgeClass;
  // Initialize RTCxxBridge
  self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
  // Start RTCxxBridge
  [self.batchedBridge start];

We can see that the initialization of RCTBridge points to RTCxxBridge.

RTCxxBridge can be said to be the core of React Native initialization. I consulted some materials. It seems that RTCxxBridge used to be called RCTBatchedBridge, so we can roughly treat these two classes as one thing.

Because the start method of RTCxxBridge is called in RCTBridge, let's see what we have done from the start method.


- (void)start {
  // 1. Initialize JSThread, and all subsequent js code will be executed in this thread
  _jsThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoop) object:nil];
  [_jsThread start];
  // Create parallel queue
  dispatch_group_t prepareBridge = dispatch_group_create();
  // 2. Register all native modules
  [self registerExtraModules];
  (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
  // 3. Initialize JSExecutorFactory instance
  std::shared_ptr<JSExecutorFactory> executorFactory;
  // 4. Initialize the underlying Instance, that is_ reactInstance
  [self ensureOnJavaScriptThread:^{
    [weakSelf _initializeBridge:executorFactory];
  // 5. Load js code
  __block NSData *sourceCode;
      loadSource:^(NSError *error, RCTSource *source) {
        if (error) {
          [weakSelf handleError:error];

        sourceCode =;
      onProgress:^(RCTLoadingProgress *progressData) {
  // 6. Execute JS after loading the native mouse and JS code
  dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
    RCTCxxBridge *strongSelf = weakSelf;
    if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO];

The above code is relatively long, which uses some knowledge points of GCD multithreading. The text description is roughly as follows:

  1. Initialize js thread_ jsThread
  2. Register all native modules on the main thread
  3. Prepare the bridge and js running environment between js and Native
  4. Create message queue RCTMessageThread on JS thread and initialize_ reactInstance
  5. Load JS Bundle on JS thread
  6. After all the above things are finished, execute JS code

In fact, the above six points can be deeply explored, but the source code involved in this section can be here. Interested readers can explore it in combination with the resources I gave at last and the React Native source code.

Android source code analysis &

Like iOS, we start the process from the entry file. Let's look at mainactivity java:

MainActivity inherits from ReactActivity, which in turn inherits from appcompactactivity:


public class MainActivity extends ReactActivity {
  // Return the component name, which is consistent with the registered name of the js entry
  protected String getMainComponentName() {
    return "rn_performance_demo";

Let's start from the Android portal file mainapplication Java start analysis:


public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost =
      new ReactNativeHost(this) {
        // Return the ReactPackage required by the app and add the module to be loaded,
        // This is where we need to add a third-party package when adding dependent packages in the project
        protected List<ReactPackage> getPackages() {
          List<ReactPackage> packages = new PackageList(this).getPackages();
          return packages;

        // js bundle entry file, set to index js
        protected String getJSMainModuleName() {
          return "index";

  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;

  public void onCreate() {
    // Bottom loader: SO C + + library loading
    SoLoader.init(this, /* native exopackage */ false);

The ReactApplication interface is very simple. It requires us to create a ReactNativeHost object:

public interface ReactApplication {
  ReactNativeHost getReactNativeHost();

From the above analysis, we can see that everything points to the ReactNativeHost class. Let's take a look at it.

The main work of ReactNativeHost is to create ReactInstanceManager:

public abstract class ReactNativeHost {
  protected ReactInstanceManager createReactInstanceManager() {
    ReactInstanceManagerBuilder builder =
            // Application context
            // JSMainModulePath is equivalent to the js Bundle on the application homepage. You can pass the url to pull the js Bundle from the server
            // Of course, this can only be used in dev mode
            // Whether to enable dev mode
            // Red callback box

    // Add ReactPackage
    for (ReactPackage reactPackage : getPackages()) {
    // Get the loading path of js Bundle
    String jsBundleFile = getJSBundleFile();
    if (jsBundleFile != null) {
    } else {
    ReactInstanceManager reactInstanceManager =;
    return reactInstanceManager;

Let's go back to ReactActivity. It doesn't do anything by itself. All functions are completed by its delegate class ReactActivityDelegate, so let's directly look at how ReactActivityDelegate is implemented:

public class ReactActivityDelegate {
  protected void onCreate(Bundle savedInstanceState) {
    String mainComponentName = getMainComponentName();
    mReactDelegate =
        new ReactDelegate(
            getPlainActivity(), getReactNativeHost(), mainComponentName, getLaunchOptions()) {
          protected ReactRootView createRootView() {
            return ReactActivityDelegate.this.createRootView();
    if (mMainComponentName != null) {
      // Load app page
  protected void loadApp(String appKey) {
    // setContentView() method of Activity

onCreate() instantiates a ReactDelegate. Let's look at its implementation.

In reactdelegate In Java, I didn't see it do two things:

  • Create ReactRootView as the root view
  • Call getreactnativehost() Getreactinstancemanager() start RN application
public class ReactDelegate {
  public void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    // Create ReactRootView as the root view
    mReactRootView = createRootView();
    // Start RN application
        getReactNativeHost().getReactInstanceManager(), appKey, mLaunchOptions);

Basic startup process this section involves the content of the source code here. Interested readers can explore it in combination with the resources I gave at the end and the source code of React Native.

Optimization suggestions

For the application with React Native as the main body, the RN container should be initialized immediately after the APP is started, and there is basically no optimization idea; However, Native based hybrid development APP has some tricks:

Since initialization takes the longest time, why don't we initialize in advance before we officially enter the React Native container?

This method is very common because many H5 containers do the same. Before officially entering the WebView web page, make a WebView container pool, initialize WebView in advance, and directly load the data rendering after entering the H5 container, so as to achieve the effect of opening the web page in seconds.

The concept of RN container pool looks mysterious. In fact, it is a Map. The key is the componentName of RN page (i.e. the appName passed in AppRegistry.registerComponent(appName, Component)), and the value is an instantiated RCTRootView/ReactRootView.

After the APP is started, find a trigger time to initialize in advance. Read the container pool before entering the RN container. If there is a matching container, use it directly. If there is no matching container, re initialize it.

Write two simple cases. iOS can build RN container pool as shown in the figure below:

@property (nonatomic, strong) NSMutableDictionary<NSString *, RCTRootView *> *rootViewRool;

// Container pool
-(NSMutableDictionary<NSString *, RCTRootView *> *)rootViewRool {
  if (!_rootViewRool) {
    _rootViewRool = @{}.mutableCopy;
  return _rootViewRool;

// Cache RCTRootView
-(void)cacheRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge {
  // initialization
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
  // After instantiation, it should be loaded to the bottom of the screen, otherwise the view rendering cannot be triggered
  [[UIApplication sharedApplication].keyWindow.rootViewController.view insertSubview:rootView atIndex:0];
  rootView.frame = [UIScreen mainScreen].bounds;
  // Put the cached RCTRootView into the container pool
  NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path];
  self.rootViewRool[key] = rootView;

// Read container
-(RCTRootView *)getRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge {
  NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path];
  RCTRootView *rootView = self.rootViewRool[key];
  if (rootView) {
    return rootView;
  // Bottom logic
  return [[RCTRootView alloc] initWithBridge:bridge moduleName:componentName initialProperties:props];

Android builds RN container pool as follows:

private HashMap<String, ReactRootView> rootViewPool = new HashMap<>();

// Create container
private ReactRootView createRootView(String componentName, String path, Bundle props, Context context) {
    ReactInstanceManager bridgeInstance = ((ReactApplication) application).getReactNativeHost().getReactInstanceManager();
    ReactRootView rootView = new ReactRootView(context);

    if(props == null) {
        props = new Bundle();
    props.putString("path", path);

    rootView.startReactApplication(bridgeInstance, componentName, props);

    return rootView;

// Cache container
public void cahceRootView(String componentName, String path, Bundle props, Context context) {
    ReactRootView rootView = createRootView(componentName, path, props, context);
    String key = componentName + "_" + path;

    // Put the cached RCTRootView into the container pool
    rootViewPool.put(key, rootView);

// Read container
public ReactRootView getRootView(String componentName, String path, Bundle props, Context context) {
    String key = componentName + "_" + path;
    ReactRootView rootView = rootViewPool.get(key);

    if (rootView != null) {
        return rootView;

    // Bottom logic
    return createRootView(componentName, path, props, context);

Of course, because each RCTRootView/ReactRootView takes up a certain amount of memory, when to instantiate, instantiate several containers, limit the size of the pool, and when to clear containers need to be practiced and explored in combination with the business.

iOS source code analysis

Native Modules of iOS has three pieces of content, and the big one is in the middle_ initializeModules function:


- (void)start {
  // Initwithbundleurl is called when initializing RCTBridge_ moduleProvider_ native modules returned by moduleprovider in launchoptions
  [self registerExtraModules];
  // Register all custom native modules
  (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
  // Initialize all lazy loaded native module s and call them only when using Chrome debug
  [self registerExtraLazyModules];

Let's see_ What does the initializeModules function do:


- (NSArray<RCTModuleData *> *)_initializeModules:(NSArray<Class> *)modules
    for (RCTModuleData *moduleData in _moduleDataByID) {
      if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) {
        // Modules that were pre-initialized should ideally be set up before
        // bridge init has finished, otherwise the caller may try to access the
        // module directly rather than via `[bridge moduleForClass:]`, which won't
        // trigger the lazy initialization process. If the module cannot safely be
        // set up on the current thread, it will instead be async dispatched
        // to the main thread to be set up in _prepareModulesWithDispatchGroup:.
        (void)[moduleData instance];
    _moduleSetupComplete = YES;
    [self _prepareModulesWithDispatchGroup:dispatchGroup];

According to_ initializeModules and_ According to the notes of prepareModulesWithDispatchGroup, iOS initializes all Native Modules in the main thread during the loading process of JS Bundle (in the JSThead thread).

Combined with the previous source code analysis, we can see that when the React Native iOS container is initialized, all Native Modules will be initialized. If there are many Native Modules, it will affect the startup time of the Android RN container.

Android source code analysis

The registration of Native Modules is actually in mainapplication The java entry file has given clues:


protected List<ReactPackage> getPackages() {
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
  return packages;

Since auto link is enabled in React Native after 0.60, and the installed third-party Native Modules are all in PackageList, we can get auto link Modules as long as getPackages().

In the source code, in reactinstancemanager In the java file, createReactContext() will be run to create ReactContext. One step is to register the registry of nativeModules:


private ReactApplicationContext createReactContext(
  JavaScriptExecutor jsExecutor, 
  JSBundleLoader jsBundleLoader) {
  // Register nativeModules registry
  NativeModuleRegistry nativeModuleRegistry = processPackages(reactContext, mPackages, false);

According to the function call, we trace to the function processPackages(), and use a for loop to add all the Native Modules in mPackages to the registry:


private NativeModuleRegistry processPackages(
    ReactApplicationContext reactContext,
    List<ReactPackage> packages,
    boolean checkAndUpdatePackageMembership) {
  // Create JavaModule registry Builder, which is used to create JavaModule registry,
  // JavaModule registry registers all javamodules in CatalystInstance
  NativeModuleRegistryBuilder nativeModuleRegistryBuilder =
      new NativeModuleRegistryBuilder(reactContext, this);

  // Lock mPackages
  // The type of mPackages is list < reactpackage >, which is the same as mainapplication packages in Java
  synchronized (mPackages) {
    for (ReactPackage reactPackage : packages) {
      try {
        // Loop the ReactPackage we injected into the Application. The process is to add their modules to the corresponding registry
        processPackage(reactPackage, nativeModuleRegistryBuilder);
      } finally {

  NativeModuleRegistry nativeModuleRegistry;
  try {
    // Generate Java Module registry
    nativeModuleRegistry =;
  } finally {

  return nativeModuleRegistry;

Finally, call processPackage() for real registration.


private void processPackage(
    ReactPackage reactPackage,
    NativeModuleRegistryBuilder nativeModuleRegistryBuilder
) {

It can be seen from the above process that Android registers Native Modules in full synchronization. If there are many Native Modules, it will affect the startup time of Android RN container.

Optimization suggestions

To be honest, the full binding of Native Modules has no solution in the existing architecture: whether you use the Native Methods or not, initialize them all when the container starts. In the new RN architecture, TurboModules will solve this problem (described in the next section of this article).

If you have to say optimization, there's another idea. Aren't you fully initialized? Why don't I just reduce the number of Native Modules? One step in the new architecture is called Lean Core, which is to streamline the React Native core, remove some functions / components from the main project of RN (such as WebView components) and hand them over to the community for maintenance. You can download and integrate them separately when you want to use them.

The main benefits of this are as follows:

  • The core is more streamlined, and RN maintainers have more energy to maintain main functions
  • Reduce the binding time and redundant JS loading time of Native Modules, reduce the package volume, and be more friendly to the initialization performance (after we upgrade the RN version to 0.62, the initialization speed is doubled, which is basically the credit of Lean Core)
  • Accelerate iteration speed, optimize development experience, etc

Now the work of Lean Core has been basically completed, and more discussions can be seen Official issues discussion area , we can enjoy the achievements of Lean Core as long as we synchronously upgrade the React Native version.

4. How to optimize the startup performance of the new RN architecture

The new architecture of React Native has been skipped for nearly two years. Every time I ask about the progress, the official reply is "don't rush, don't rush, do it, do it".

I personally looked forward to it for a whole year last year, but I didn't wait for anything, so I don't care when RN will be updated to version 1.0.0. Although the RN official has been pigeoning, I have to say that there are still some things about their new architecture. There are articles and videos about the new architecture of RN on the market. I have basically read them once, so I still have an overall understanding of the new architecture.

Because the new architecture has not been officially released, there must be some differences in specific details. The specific implementation details still need to be subject to the official of React Native.


The full name of JSI is JavaScript Interface, a framework written in C + +. Its function is to support JS to directly call Native methods instead of asynchronous communication through Bridge.

How to understand JS calling Native directly? Let's take the simplest example. Call setTimeout document. On the browser When using API s such as getelementbyid, we actually call Native Code directly on the JS side. We can verify it in the browser console:

For example, I executed a command:

let el = document.createElement('div')

The variable el holds not a JS object, but an object instantiated in C + +. For the object held by el, let's set the following related properties:

el.setAttribute('width', 100)

At this time, JS synchronously calls the setWidth method in C + + to change the width of this element.

JSI in the new architecture of React Native mainly plays this role. With JSI, we can directly obtain the Host Objects of C + + objects with JS, and then directly control the UI and call the methods of Native Modules, so as to save the overhead of bridge asynchronous communication.

Let's take a small example to see how Java/OC exposes synchronous calling methods to JS with JSI.

#pragma once

#include <string>
#include <unordered_map>

#include <jsi/jsi.h>

// Samplejsioobject inherits from HostObject and represents an object exposed to JS
// For JS, JS can directly and synchronously call the properties and methods on this object
class JSI_EXPORT SampleJSIObject : public facebook::jsi::HostObject {


// First step
// Set window__ Samplejsiobject exposed to JavaScript
// This is a static function, which is usually invoked from ObjC/Java when initialization is applied.
static void SampleJSIObject::install(jsi::Runtime &runtime) {
          jsi::PropNameID::forAscii(runtime, "__SampleJSIObject"),
          [binding](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) {
            // Return to call window__ Contents obtained when samplejsioobject
            return std::make_shared<SampleJSIObject>();

// Similar to getter, every time JS accesses this object, it will go through this method, which is similar to a wrapper
// For example, we call window__ sampleJSIObject. Method1 (), this method will be called
jsi::Value TurboModule::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
  // Call method name
  // For example, call window__ sampleJSIObject. When method1(), propNameUtf8 is method1
  std::string propNameUtf8 = propName.utf8(runtime);

  return jsi::Function::createFromHostFunction(
    [](facebook::jsi::Runtime &rt, const facebook::jsi::Value &thisVal, const facebook::jsi::Value *args, size_t count) {
      if (propNameUtf8 == 'method1') {
        // When calling method1, the related function processing logic
std::vector<PropNameID> getPropertyNames(Runtime& rt){


Through the previous source code analysis, we can know that in the existing architecture, native modules will be loaded in full during Native initialization. With the business iteration, native modules will only be more and more, and the time consumption here will be longer and longer.

TurboModules can solve this problem at one time. In the new architecture, native modules are lazy to load, that is, only when you call the corresponding native modules can you initialize the load, which solves the problem of long time-consuming initialization of full load.

The calling path of TurboModules is roughly as follows:

  1. First, create a top-level "Native Modules Proxy" with JSI, which is called global__ turboModuleProxy
  2. To access a native module, for example, to access SampleTurboModule, we first execute require('NativeSampleTurboModule ') on the JavaScript side
  3. In nativesampleturbomodule JS file, we first call turbomoduleregistry Getenforcing(), and then global. Net will be called__ turboModuleProxy("SampleTurboModule")
  4. Call global__ When turbomoduleproxy, it will call the Native method exposed by JSI in the first step. At this time, the C + + layer finds the ObjC/Java implementation through the incoming string "SampleTurboModule", and finally returns a corresponding JSI object
  5. Now that we have the JSI object of SampleTurboModule, we can use JavaScript to synchronously call the properties and methods on the JSI object

Through the above steps, we can see that with the help of TurboModules, Native Modules will be loaded only when they are called for the first time, which will completely eliminate the time when the React Native container is fully loaded during initialization; At the same time, we can use JSI to realize the synchronous call of JS and Native, which takes less time and is more efficient.


From the perspective of Native, this paper analyzes the startup process of React Native's existing architecture from the source code, and summarizes several performance optimization points of Native layer; Finally, it briefly introduces the new architecture of React Native. In the next article, I will explain how to start with JavaScript to optimize the startup speed of React Native.

Author: marinated egg Laboratory Link: Source: rare earth Nuggets The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.