Wait… What Happens When my React Native Application Starts? — An In-depth Look Inside React Native

Discover how React Native functions internally, and what it does for you without you knowing it.

Nicolas Couvrat
Level Up Coding

--

Disclaimer: this articles assumes a (very) basic understanding of React Native and Native Modules. If you have never played around with them, I’d recommend getting a look at the official documentation first.

Edit: This article is based on a talk I’ve given in London for the ReactFest conference in March 2018. You can see me talk over here!

As I started using React Native, something quickly bugged me: what’s happening there? Indeed, from the standpoint of someone who simply uses React Native to build (great) applications, things can sometimes look magical. Want to make some native code usable from Javascript? Simply use @ReactMethod (or RCT_EXPORT_METHOD on iOS)! Want to send an event over from native to Javascript? No problem, just grab the appropriate Javascript module and call it like you would do for any native method! Not to mention, the same Javascript code will run on both iOS and Android…

As one might guess, these things are not trivial. Fairly complex, in fact. So what enables React Native to achieve such feats? The easy and commonly given answer is:

The React Native “Bridge”, duh!

In other words:

Some React Native explanations…

But who wants easy answers? This time around we will go into details. But be warned: things might get messy.

The React Native Infrastructure

Let’s get straight to the point: in order to function, React Native relies on a full infrastructure built at runtime. Ta-da!

Wait… didn’t you just give us another cryptic explanation?

I did? But there are two things to note here. First, infrastructure: the famous “bridge” is just a part of it — and not the most incredible, I dare say. Second, runtime: the above infrastructure is being built every single time you launch a React Native application, before any custom code is executed. In other words, before your application comes online and becomes visible, it goes through a transitional state where React Native is busy building your application’s foundations for you.

What does this so-called infrastructure looks like, you say? Well, let us draw a map, showing various parts (linked by arrows roughly meaning “stores a reference to”).

React Native Infrastructure — the Java logo could be replaced with an Objective-C one in the case of iOS

Woops, that got a little complex, didn’t it? And this is a simplified version… In order to understand this mess, we will describe its parts one by one, in the order in which they are created at launch. Let’s go!

Starting a React Native Application

Remember that the whole thing is built whenever a React Native application starts? Let us go through the multiple steps that separate the instant when you press your application’s logo on your phone from the moment everything becomes visible.

First, your application only has two things to work with:

  • The application’s code,
  • A unique thread, the main thread*, that is automatically assigned to it by the phone’s operating system.

To simplify explanations, we are going to conceptually split the code in two: the framework code — the one you do not have to write every time — and the custom code — that actually describes your application. Both being distributed over Javascript and native, this gives us a total of four parts of code to go through. The first thing that will get processed — on the main thread — is the native part of the framework code.

*also called UI Thread, as — outside of initialization — it will mainly be responsible of UI-related work

Creating the Native Foundations

One important thing to realize is that, although most of the UI code — <View>, <Text>… — is written in Javascript, what will in the end be rendered are only native views. That’s it. This means that the React Native framework needs to:

  1. create native views and map their connections to Javascript components,
  2. store these natives views and display them.

While the first step will be handled by the UIManagerModule (which we will describe a little later), the RootView will take care of the second one. The RootView is more or less a container in which native views are organized in a big tree — a native representation of the Javascript component tree, if you like — and everything that will show up on the phone’s screen will be stored in there.

Back to our initializing process: everything starts with the creation of the above RootView— an empty container for now — before moving on to the Bridge Interface.

Wasn’t the bridge supposed to be between native and Javascript? Why would you need an interface there?

It is! But although most of the native side — including the RootView — is written in a platform-specific language (Objective-C or Java), the bridge is entirely implemented in C++ . The Bridge Interface therefore acts as an API, allowing the former to interact with the latter. The bridge itself is made up of two ends, native to Javascript, and vice versa.

The bridge, however, would be nothing without endpoints to dispatch calls to. These endpoints are Native Modules and will, in the end, be the only things available to the Javascript environment. In other words, everything but Native Modules will be eventually invisible to your Javascript application. For this reason, aside from the Custom Modules that you may or may not decide to create, the framework also includes Core Modules. One example of the latter would be the UIManagerModule, which stores a map of all Javascript UI Components and their associated native views. Every time a Javascript UI Component is created, updated or deleted, the UIManagerModule will use this map to accordingly create, update or delete the corresponding native view. It will also forward changes to the native view tree stored in the RootView in order to make them visible.

From the standpoint of initialization, all Native Modules are treated the same: for each module, an instance is created, and a reference to that instance is stored on the Javascript to native bridge — so that they can later be called from Javascript. In addition, a reference to the Bridge Interface is also passed to each Native Module, allowing them to call Javascript directly. Finally, two additional threads will be created: the JS Thread and the NativeModulesThread*.

*strictly speaking, it is not a unique thread but a pool of threads in the case of the iOS implementation of React Native.

Intermezzo: Setting Up the Javascript Engine

Before moving on, let’s make a quick summary of what has happened so far:

  • a bunch of native stuff has been created on the main thread,
  • we now have three threads to work with,
  • absolutely no Javascript has been processed yet.

Referring back to our initial map, what we have is this:

React Native’s native side

Meaning it is now time to load the Javascript bundle — framework and custom code alike!

Being an interpreted scripting language, Javascript cannot be run as is: it needs to be converted to bytecode, then be executed. This is the job of the Javascript virtual machine (a.k.a. Javascript engine). There are many Javascript engines out there, including Chrome’s V8, Mozilla’s SpiderMonkey and Safari’s JavaScriptCore… If in debug mode, React Native will use V8 and directly run in the browser, otherwise, it defaults to JavaScriptCore and runs on the device. As a side note, JavaScriptCore is not included in Android by default (while it is in iOS), so React Native automatically bundles a copy of it in the Android application — making Android applications slightly heavier than their iOS counterparts.

In any case, before effectively kicking off the Javascript engine, React Native has to give it a Context representing the execution environment. That includes the Javascript global object, and means that this global object is in fact created and stored on the C++ bridge. Why is that so important? Because the global object is then not only reachable from within the Javascript environment, but also from outside. It is therefore the primary means of communication between C++ (native) and Javascript, as it is through the global object that some native functions will be made available to Javascript — functions that will in turn be used to pass back data from Javascript to native.

Many things are stored on the global object, but the ModuleConfig array and the flushQueue() function are especially important. Each element of the ModuleConfig array describes a Native Module (be it Core or Custom), including its name, its exported constants, methods… The flushQueue() function plays a critical role in assuring communication between the Javascript and native environment, as it it will be used periodically to pass calls from the first back to the second.

Once the Javascript Context has been fully created and filled, it is fed to the Javascript engine that starts loading the React Native Javascript bundle on the JS Thread.

Loading the Javascript Bundle

As the virtual machine begins processing the Javascript part of the framework code, it will create the BatchedBridge. That name might ring a bell as it sometimes pops up in error messages! Despite its fancy denomination, it is but a simple queue, that stores “calls from Javascript to native”. A “call” is an object containing a native module ID, a method ID (for the specified native module), along with arguments that the native method is to be called with. Periodically (every 5 milliseconds by default), the BatchedBridge will call on global.flushQueue(), passing its content — an array of “calls” — to the Javascript to native end of the C++ bridge. Know as batches, these small arrays are indexed so as to ensure that all UI changes contained in one batch are made visible at the same time (this is necessary because the entire process is asynchronous). The Javascript to native end of the bridge will finally iterate over each call in a batch and dispatch them to the appropriate native module using the specified module ID — which it can do because it has a reference pointing to each and any native module, remember?

The next step is creating the NativeModules object — yes, the very object that has to be imported from ‘react-native’ each time you want to call a native module. The NativeModules object will be filled using the ModuleConfig array mentioned earlier. I will not go into the details of this process here, but it is roughly equivalent to doing NativeModules[module_name]={} for each module_name contained in the ModuleConfig, then NativeModules[module_name][method_name]=fillerMethod for each exported native method of the given module. fillerMethod is simply there to store all arguments it receives on the BatchedBridge , along with the method and module ID (something like fillerMethod = function(...args) { BatchedBridge.enqueueNativeCall(moduleID, methodID, args)} ), effectively creating a “call” from Javascript to native. That being said, what is fired when you later write MyNativeModule.myMethod(args) is actually the abovefillerMethod !

We’re almost there. The last thing that needs to be done is creating the core JS Modules, among which the DeviceEventEmitter— that will be used to send events from native to Javascript — or the AppRegistry, that stores a reference to the main components of your app. In order to be callable from native, these modules are registered on the Javascript global object…

…and with that, the full React Native infrastructure has been built!

Making the React Native Application Visible

Despite the initialization being all but complete, our application is still invisible at this stage! Indeed, the loading of the Javascript bundle happened on the JS thread, which is independent of the main (a.k.a. UI) thread. The JS thread thus has to warn the main thread about the completion of its task, and in response, the main thread uses the AppRegistry (JS module) to ask the JS thread to process the main custom component — usually App.js .

To sum it up from a threading perspective, a React Native application’s launch process looks like this:

React Native’s starting routine

The Javascript component tree contained in your application’s main component will be traversed, calling the UIManagerModule every time a UI component is encountered. The UIManagerModule (on the UI thread) will in turn take care of creating native views and storing them in the RootView : congratulations, your application is now visible! 🎉 🎉

Additional takeaways

This article is based on a talk I gave in March 2018 at London’s ReactFest. Below are answers to some questions I’ve been asked during the conference.

What’s the point of creating a NativeModuleThread if we’re not using it?

It is true that this thread is not actively used during the start up process. It is however really important later on, as every call from Javascript to native will — after having been dispatched to the appropriate native module — be run on this thread. Implementation detail: on the iOS version of React Native, this thread does not exist as such. Instead, each native module is given a GCDQueue at instantiation (and the system takes care of thread management).

Hey, why are the two ends of the bridge both in C++? Weren’t we supposed to have one end in Javascript, and one end in native?

This can be slightly confusing indeed. But it makes sense once we nail down what exactly is the gap that needs to be bridged by React Native. This “gap” is here twofold:

  • a language gap (native, Javascript), and
  • a thread gap (JS thread, main thread, NativeModuleThread)

The language issue is, as we covered earlier, solved mostly with the Javascript global object, that is accessible both from the C++ and Javascript environment. Consequently, what we usually call “bridge” in React Native only needs to handle the dispatching of work over different threads. In that regard, the names “native to Javascript” and “Javascript to native” make perfect sense, despite being both implemented in the same language: the first one is called from a native thread (be it main thread or NativeModuleThread) and will forward work to the JS thread, while the second one will be called from the JS thread (with the global.flushQueue() function) and dispatch calls to a native thread.

That’s it! That was a slightly more detailed overview of what powers React Native. I hope it triggered your curiosity and — who knows — made you want to contribute to the framework?

--

--

Hi! I am a software engineer at Playstation, Japan. I mostly talk about backend stuff, golang and vim. I also like cooking. My views are my own.