React Amsterdam, April 10-12, 2019, Kromhouthal, Amsterdam
Lazy Native Modules - React Native (Android)
In a previous post, I wrote about leveraging React Native's built in instrumentation to understand the startup path for React Native apps. Armed with this data, we can start optimizing the various segments of the timeline that corresponding to loading React Native.
Though many native modules are used much later during the lifetime of the app, the setup today initializes all of them as soon as the user starts the application. Moving this initialization closer to when the module is actually used can help speed up the startup time of React Native apps.
This way of initialization not only "classloads" RNDeviceInfo, but also invokes the constructor. As it turns out, the device info module also performs some non-trivial computations which adds up to the startup time of the app.
An example using this provider pattern would be MainReactPackage. To adopt this to our applications, we would reach into the native module and add the class extending from ReactContextBaseJavaModule in a list wrapped in LazyReactPackage.
Note that when using LazyReactPackage, we still need to provide additional information about that native module that is otherwise obtained from evaluating the module. Hence, we have the method to getReactModuleInfos, that supplies additional information like class name, module name, is it a C++ module, can it be overridden, etc. This information can either be hand crafted as in the example, or generated using a pre-processing step in ReactModuleSpecProcessor if integrated into the build system.
This project has a full React Native application that uses this pattern to make module loading fast. It is also instrumented with ReactMarkers, and can show that the PROCESS_PACKAGES step is now a fraction of the original time during startup.
This package is also a step torwards the new TurboModule architecture, where JavaScript can get a native module by its name.
I would recommend using TurboReactPackage when adding native modules.
To make this work with react-native link today, the following changes would be needed.
To prevent breaking changes, we could use a flag to specify that we want to use this new way of adding native modules to newer projects.
These techniques can already be used in hybrid apps since they don't use react-native link anyway. We could switch ReactPackage to use TurboReactPackage and change the methods to take advantage of the performance savings.
We can simply spawn a new thread and call CatalystInstance.getModule(moduleName) which initializes the modules in a thread safe way.
Target: This post documents ideas to improve startup times for applications that have a many native modules.
Background
Native Modules enable JavaScript to call methods in Java or ObjectiveC and React Native's vibrant ecosystem has native modules for most scenarios an app would need. Installing native code is as simple as running npm install and react-native link. Consequently, many apps eventually start building up a large registry of these modules.Though many native modules are used much later during the lifetime of the app, the setup today initializes all of them as soon as the user starts the application. Moving this initialization closer to when the module is actually used can help speed up the startup time of React Native apps.
Adding Native Modules today
To add a native module today, most people use the convenience provided by react-native link. In addition to modifying the Gradle file, it also updates MainApplication.java to initialize the module. For example, when the DeviceInfo Module is added using react-native link, the function with list of packages in the file looks as follows.@Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage() new RNDeviceInfo() ); }
This way of initialization not only "classloads" RNDeviceInfo, but also invokes the constructor. As it turns out, the device info module also performs some non-trivial computations which adds up to the startup time of the app.
Opportunities for Optimization
- During startup, React Native does not really need to full module, it just needs to know the name of the module and a few other details so that the module can be added it to a list. When we call require('react-native-device-info') in our code, it still does not an instance of the module, but just the list of methods and their signatures. The actual module needs to be constructed when only one of the methods is actually called.
- Additionally, the open source structure of native modules dictates all modules to be wrapped in React packages even if they don't provide ViewManagers or JavaScript modules, adding an extra layer of abstraction.
Lazy React Packages
By wrapping the constructor using a Java Provider, we still set the native module without invoking the constructor. This can be achieved by using LazyReactPackage instead of the default ReactPackage.An example using this provider pattern would be MainReactPackage. To adopt this to our applications, we would reach into the native module and add the class extending from ReactContextBaseJavaModule in a list wrapped in LazyReactPackage.
Note that when using LazyReactPackage, we still need to provide additional information about that native module that is otherwise obtained from evaluating the module. Hence, we have the method to getReactModuleInfos, that supplies additional information like class name, module name, is it a C++ module, can it be overridden, etc. This information can either be hand crafted as in the example, or generated using a pre-processing step in ReactModuleSpecProcessor if integrated into the build system.
Turbo React Packages
Even if the above method of Lazy React Package does not call the constructor, it still ends up creating anonymous classes due to the providers. In a large. multi-dex app, it will also perform classloads for the native modules at startup. We can eliminate these side effects using a TurboReactPackage and using the getModule method that returns the native module given the name.This project has a full React Native application that uses this pattern to make module loading fast. It is also instrumented with ReactMarkers, and can show that the PROCESS_PACKAGES step is now a fraction of the original time during startup.
This package is also a step torwards the new TurboModule architecture, where JavaScript can get a native module by its name.
I would recommend using TurboReactPackage when adding native modules.
Fast by Default
React Native already supports LazyReactPackage and the newer TurboReactPackage today.To make this work with react-native link today, the following changes would be needed.
- If the native modules does not have View Managers or JSModules, don't add the Package that is exposed
- Instead look for the class that eventually extends ReactContextBaseModule and add that to a list maintained in MainApplication
- Generate the meta data needed for ReactModuleInfo in the same package, so that we have all the information without having to evaluate the actual package.
To prevent breaking changes, we could use a flag to specify that we want to use this new way of adding native modules to newer projects.
These techniques can already be used in hybrid apps since they don't use react-native link anyway. We could switch ReactPackage to use TurboReactPackage and change the methods to take advantage of the performance savings.
More Optimizations
Even if we defer loading native modules, some native modules are called during the startup process. The JavaScript thread pauses while this call happens. By making native modules lazy, we would end up making JavaScript wait longer as the native module is initialized before the actual call. For native modules that area part of startup, we could still initialize them in a separate thread in parallel to when JavaScript is being parsed.We can simply spawn a new thread and call CatalystInstance.getModule(moduleName) which initializes the modules in a thread safe way.