Phillip Pan – React Native under the hood | App.js Conf 2024

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
I'm Philip I'm a software engineer at meta currently I work in the react or where I work on the native architecture provide uh supporting the re react native framework um this is a relatively technical talk um it's not about announcements or calls to action it's for the Curious ones that enjoy going under the hood of the Frameworks that they work with I hope you will leave this talk a little bit more enlightened as a developer and that the knowledge that you gain from this talk will help you build more effective systems in the future cool so in order to understand this talk we first have to understand how the JS environment Works JavaScript cannot execute without a runtime environment what does this runtime consist of it has a JS engine there are many JS engines out there that power different browsers va8 is well known to power Chrome the engine is responsible for the execution of JS code and handles memory management of values another fundamental thing the runtime provides are the apis such as zo to manipulate the Dom there are other Concepts such as cues and event Loops that are used to manage synchronization within the runtime so let's go back to the conception of react native about a decade ago how did we integrate the JS runtime environment into a mobile one we Ed a JavaScript engine called jsse best known for powering Safari in Apple's ecosystem this is a c-based API that allows us to embed JavaScript at runtime uh into our native runtime so you guys know all about the react native Bridge right so this was the conception of the react native bridge where we would manage and communicate with JSI sorry JSC JSI is coming so let's talk about the flow of data in this old world all JS events would need to go through the bridge the bridge would serialize the commands to Json and invoke these meth methods in JSC the Callback would get asynchronously batched into a Json response we would then deserialize the response and invoke the method on the native module so here I have some sample code on how we would call the JavaScript error method in JSC it consists of getting the runtime getting the error method and then invoking it with an argument I'm not going to go through every detail here but the reason it looks over Bast is because the JSC API is we have to manually we have to manually manage memory ourselves and C is always going to be syntactically heavy anytime we try to do something object oriented so this model works it worked um but one of the biggest problems of this setup was the lack of flexibility so first C is challenging to work with it's verbose aor prone and we had to me manage memory ourselves next everything has to go through the bridge this is challenging for maintainability as the bridge starts to get too many responsibilities finally the implementation of react native is tightly coupled with the JS engine of choice JSC we don't have the option of using another engine without rewriting the framework so instead of coupling the architecture with a specific JS engine what we want is an interface that gives us the flexibility to plug in any engine that we want and also we would like to avoid C and avoid having the bridge as a dependency for JS and Native communication so yeah in this diagram it's like want to be able to use V8 Hermes or JSC whenever we want so a few years later engineers at meta came up with JSI a C++ interface for apps to embed JS designed to be engine agnostic and lightweight instead of the Native implementation being coupled with the engine we would use an injected implementation of JSI for example Microsoft began supporting react native's desktop environment with a JSI implementation of chakra at meta when we were limited by the capabilities of JSC we decided to build our own JS runtime called Hermes the JSI abstraction made it really easy for us to test changes by simply changing the dependency in react native otherwise we have to Fork our code everywhere so JSI can be considered a wrapper of the underlying engines API but because it's written in C++ there are some functional changes as well the main functional change after integrating JSI is the flow of data from native we would call JSI which can dispatch and invoke commands via C++ this is invoked into a C++ shared memory later because JSI is written in C++ the updates in memory will be reflected and visible in JavaScript the way this works depends on the implementation of JSI so JavaScript can immediately access the C++ layer whereas before it could only communicate to Native by batching asynchronous calls to the bridge so remember this how we invoke the JavaScript error using JSC this is how it looks with JSI the JS runtime represents an object uh is represented by an object with a type JSI runtime we then retrieve the error method from the global property of the runtime and then invoke it because we're using C++ we don't have to handle marking objects for garbage collection ourselves in my opinion this code is much easier to read and write so now we know the motiv the motivation and the high LEL idea of why we use JSI let's look at what J what API JSI offers that that allows us to power react native cool let's start with the JSI runtime remember the diagram of the runtime I described in the beginning of the talk JSI runtime is an obstruction of that it's implemented with with a JS engine of choice this instance of JSI runtime is fundamental to using JSI its implementation determines how we read and write values to underlying memory and how we invoke JavaScript methods it's highly advanced stuff what you do need to know is that an instance of JSI runtime is a dependency for most JSI apis to work as I introduce other JSI types I'll be going over more of these apis there are some things to know around using JSI runtime it can only be read and written to from the JavaScript thread otherwise you may retrieve incorrect values or trigger undefined behavior in react native the framework is responsible for managing the life cycle of the runtime it can be torn down and restarted at any given time which means that capturing it means that you have to manage a life cycle yourself finally the runtime needs to be passed to almost every single JSI method this is intuitive when reading values from JavaScript but we have to do this when creating values as well because JSI will need to track the values in C++ the JSI runtime is not a single in advanced use cases is we can handle multiple run times and I'm pretty sure I saw some talks that did that today the most fundamental type in JSI is JSI value it's a C++ representation of JavaScript values JSI value has a number of apis to convert to permitted values or sub classes in this example we have an example of JSI representing a Boolean we can convert it to a native Boolean but trying to convert it to a string will throw an error more practically JSI value is what we pass to JSI to invoke JavaScript methods from native and it's also representative of the values that we receive um for JavaScript methods that return a value so here we have JSI object it's a subass of JSI value that provides object apis like project uh property Getters and Setters this example is not too interesting but I'll get into the capabilities of object later JSI also offers object representations of JS functions here we retrieve the global promise function from the runtime we then create an instance of this promise that lives in JS memory but is Created from native this example shows how Native can access functions from JavaScript but what about the other way around I also want to add that this is essentially what we do to support promises in Native modules cool so we can also embed native functions into the Js runtime with what we call host functions in this example we embed a native function called host function onto the JavaScript Global property we can then call this function off the JS global object so this is an example of how JS can invoke something in Native similarly we can embed JSI objects into the runtime as host objects retrieving values on the object will trigger the G method which can be overridden in our implementation of the host object in this example we override get to create a method called My Method a on the host object after installing the object onto the runtime we can call my method a it'll invoke the underlying implementation that we overrode in the get method so you can see how powerful this pattern is as we have a way to share objects between JS and Native where the user doesn't have to worry about any memory management it's all handled by JSI so now we know how we can use JSI to communicate between JS and Native how do we actually use this API to power react native I'll share a few examples so remember the pre JSI native module communication flow let's revisit it real quickly the bridge would invoke the JSC API to start some work on the JavaScript runtime in the Callback the runtime would batch some serialized Json response encoded um encoding the native module instructions and then finally the bridge would de serialize this response look up the module and then invoke the methods so I'm going to share with you the native module flow when we first integrated JSI when the runtime invokes the native module it passes a JavaScript value to the underlying JS engine where the JSI implementation will then track it in C++ this gets passed to our bound host function so native call our host function would then convert the JSI value to a C++ dynamic and so if you don't know what a C++ Dynamic is it's just a bag of values similar to JSI value but it's not limited to representing JavaScript vales this standardized type would then get passed to Native module infra and the module infra would look at the native module convert the arguments to Native types and then invoke the method note that here we've decoupled the flow of data from the Native Bridge so there's no Bridge here anymore in the new architecture for Native modules also known as turbo modules we do things a little bit differently instead of using a host function we used a host object this example is IOS specific we can leverage the toll-free C++ bridging in Objective C C++ sorry objective C++ and go directly from JSI value to the native type this improves method invocation performance because we have we can save a d serialization path pass cool so let's talk about the rendering layer that powers the Legacy architecture so the path is very similar to the native module flow because we manage nodes with a native module called a UI manager the native module infrastructure converts the dynamic to a general mapping of prop names to generic object types there's additional work we have to do we have to do an additional deserialization pass in order to convert the generic object types to types that can be recognized by native views cool so in the new architecture we use a host object instead of a native module to represent the UI manager decoupling the critical path from native modules the UI manager converts the JSI value to Raw props which is just a generic wrapper of JSI value we don't do any conversion here this is just a wrapper then finally we directly convert the raw props to Native type props and pass it to a native view so in this flow we saved a number of deserialization steps so these are some benchmarks on the new architecture renderer it has noticeably improved rendering at scale especially on iOS where we can leverage toll-free C++ bridging with objective C++ but unfortunately we still have some not as great wins on Android due to the serialization requirements of jni to interface between Java land and C++ so this is just to show you that JSI is not some silver bullet that creates highly performant JavaScript apps the API is really powerful but it needs to be used thoughtfully cool so now I shared with you the main capabilities of JSI I'm I'm sure many of you are wondering should I use JSI if you're an application developer my answer is probably no for the most part we've abstracted much of this away via the native components and Native module API most product use cases will not require interfacing with the JavaScript runtime directly if you're a framework developer my answer is maybe there are some Advanced use cases with JS and Native communication that can't be solved with react native's current offering there are actually some libraries there are actually some libraries leveraging JSI today I'm pretty sure I saw some in the talks today um but I'd like to share one example that I think all of you are quite familiar with Expo modules so we appjs um so Expo modules is unique right because it provides an expressive DSL that allows developers to create native modules in Swift in cotland and Expo modules is able to achieve this using all the concepts I talked about today converting their DSL into custom Bindings that allows you all to implement native mod native modules in the most modern languages so the react native core team never made it official that JSI was available to use in consumer apps or libraries it was through sheer Ingenuity and curiosity that the community the community was able to figure out how to utilize JSI to build incredibly Advanced features like Expo modules or vision camera so this me that they had to do some really hacky stuff such as the example that I'm showing here this is a common pattern that advanced libraries use to retrieve the JSI runtime on iOS a private method is exposed to off of the bridge then a hard cast is done to the runtime type which is not possible to be caught during compile time if this cast was incorrect they had to do this because our core infra was not initially set up to support other people using JSI but I'm happy to announce that this year we've been working on a proper set of apis for developers to leverage JSI in their libraries and applications so note that all of the apis that we talked about today have a common dependency JSI runtime I'm reiterating this point because the apis that react native provides revolve around giving access to the JSI runtime so the first API I'm going to share with you all is the host delegate the host is a new architecture's abstraction for the runtime it's responsible for creating and managing its life cycle the host delegate will receive a call back when the runtime is finished being created so it's safe to access this is also the earliest time you can safely access the runtime usually the host delegate is the application as the application is responsible for starting the react native run time at meta we use this to bind application specific logging functions that need to be ready as soon as JS execution starts this is the iOS API where the host will call initialize runtime when it finishes creating the runtime in Android the pattern is extremely similar except we have to use jni to execute C++ the main difference here is that the Callback is injected rather than delegated because of the language boundary the Callback that's injected is called the bindings install function which is provided by the delegate next I'm going to talk about C++ turbo modules an offering from the new architecture the distinction from Turbo modules and Legacy modules is that a JavaScript spec is a source of truth of the model shape C++ turbo modules are also unique because every method will receive a reference to the runtime we can do this because by default C++ turbo module methods are run on the JS thread just like any other native module method we can invoke the method from JavaScript to execute the underlying code if you wanted to install bindings to the runtime very early you could potentially call it an index.js where we Mount the root component since it's written in C++ it's a naturally crossplatform solution for installing bindings at the moment the stable API for C plus turbo module registration is in the app layer we don't officially support autol linking yet however in 074 we've introduced experimental apis for C++ turbo module autol linking please check out the this post in the new architecture working group if you're interested in these apis the last API I want to share about uh share with you is uh via turbo modules we've introduced a hook that takes in a runtime argument the native module infro will call this Hook when creating the native module since native modules are initialized on the JS thread by default this is thread safe this does not require an app layer change we just need the module to be linked for iOS we provide a protocol called called RCT turbo module with JSI bindings this will enforce that this Hook is implemented by your module at compiled time this is the Android API equivalent note that it's extremely similar to The Host delegate but here we can install bindings in the module layer instead of the app layer these turbo module apis will be available as experimental apis in 075 now all these apis are invoked in situations where you're already guaranteed to be on the JS threat however react native is a multi-threaded environment we have the JavaScript thread obviously we have a UI thread which is a thread that can safely update UI components and retrieve information about the UI State finally a fundamental native comp capability is to allow creation of arbitrary threads to execute work in parallel what if we need to read or write values to the runtime when we are on one of these non-js threads we have an API for this use case called the call invoker the call invoker provides an API where you can dispatch once onto the dispatch onto the JavaScript thread asynchronously currently we do not support blocking the current thread to finish execution on the JS thread the API is simple you just pass in a Lambda to invoke async that takes in a reference to the runtime in C++ turbo modules getting the call invoker is easy it's always passed as a dependency to the module in iOS we provide a protocol called RCT call invoker module after conforming to this protocol the native module infrastructure infrastructure will will decorate your native module with a property to access the runtime on Android we've made the call invoker available through the react context which is injected to Native modules and Native components again this must be passed down to a C++ layer via jni to actually invoke the underlying logic we've introduced these call invoker apis in 075 these apas are actually forward compatible so if you're already using the call invoker you don't have to Fork your logic when migrating to the new architecture cool so that's the talk um hopefully you found that interesting um if you have any questions uh meta will be in the expert Lounge after this talk um I want to say thanks to all these people here for supporting this work over the years um many of them are here today and then finally I want to thank all the developers here um thanks for keeping me employed uh but also it's such a privilege to support all of the amazing work that you guys do um and being a small part of your industry Journey thank you [Applause]
Info
Channel: Software Mansion
Views: 1,687
Rating: undefined out of 5
Keywords:
Id: oLmGInjKU2U
Channel Id: undefined
Length: 21min 19sec (1279 seconds)
Published: Thu Jul 04 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.