開源日報每天推薦一個 GitHub 優質開源項目和一篇精選英文科技或編程文章原文,堅持閱讀《開源日報》,保持每日學習的好習慣。

2024年1月15日,開源日報第1075期:
今日推薦開源項目:《apihub》
今日推薦英文原文:《React: You are Using useEffect() Wrong, Do This Instead》


開源項目

今日推薦開源項目:《apihub》傳送門:項目鏈接

推薦理由: freeapi是一個單一源API 管理中心,用於學習任何編程語言中的api處理, 也用freeapi在Web和移動應用程序中構建自己前端作品集

官網直達:freeapi.app


英文原文

今日推薦英文原文:React: You are Using useEffect() Wrong, Do This Instead

推薦理由:React Native的新架構引入了重大變化 主要組件包括:

  • JavaScript Interface (JSI): 一個輕量級層,使得JavaScript能夠直接調用本地方法。這有助於在線程之間實現互操作性,消除了JSON序列化的需求,提高了性能

  • Fabric: 一種新的渲染系統,取代了UI Manager,實現了JS和UI線程之間的同步。它提升了用戶交互(如滾動和手勢)的性能,將同步執行重點放在面向用戶的任務上

  • Turbo Modules: 對舊的Native Modules的改進,Turbo Modules使JavaScript能夠在需要時載入模塊,減少了React Native應用的啟動時間

  • CodeGen: 一個靜態類型檢查器,確保JavaScript(動態類型)和C++(靜態類型)之間的順暢通信。它在構建時定義了Turbo Modules和Fabric使用的介面元素,而不是在運行時生成更多的本地代碼


ReactNative New vs. Old Architecture

ReactNative Multi platform application Development.

This is a good time to understand what changes are taking place under the hood and how they might affect your React Native App.

This article aims to cover the most important changes.

  • JavaScript Interface(JSI)
  • Fabric
  • Turbo Modules
  • CodeGen

Old Architecture

Before we get to the new architecture, let』s recap how the old one works.

React Native is a JavaScript library that allows us to create mobile apps that will run on both Android and iOS. As they sell it, 「Learn once, write anywhere.」

img

How the UI of a React Native app is rendered.

React Native allows you to build mobile apps using JavaScript and React, but the actual rendering of the user interface happens using native components of the respective platforms (iOS or Android).

  • Rendering with Native Views: React Native doesn』t directly translate your JavaScript code into native code (Java for Android or Objective C/Swift for iOS). Instead, it uses native views to render the UI. This is more efficient and provides a better user experience.
  • Communication between Components: In React Native, you build your UI using components. Components communicate with each other through properties (props) and callbacks. This follows a one-way flow, making it easier to manage data flow in your app.
  • Passing Data: If a parent component needs something from its child, it passes down a callback. Similarly, if a child component needs something from its parent, it receives a property. This unidirectional flow simplifies the design and maintenance of your app.
  • Native Code Integration: While React Native encourages building the UI using its components, if you need to use a native component (e.g., MapView), you can integrate it seamlessly. React Native provides a bridge for communication between the JavaScript code and native components.
  • How OS Understands JavaScript: The OS doesn』t directly understand JavaScript code. Instead, the React Native framework acts as an intermediary. It interprets and executes your JavaScript code, and through its bridge, it communicates with the native components to render the UI on the device.

So far, so good. But how does the OS understand the JavaScript code?

The Bundling Process

A native mobile app will be developed using the programming language that is specific to that platform. When developing with React Native, you can pretty much get by without writing any Objective C/Java code — unless you are required to do something that is not covered by the library, such as integrating a payment provider that only offers SDKs for Android and iOS.

However, any React Native project contains an ios directory and an android one. These directories act as the entry points for each platform — they basically bootstrap React Native. They contain code that is specific to each platform, and here』s where your JS code is bridged for each platform.

In order to start your app, you』ll generally run yarn android or yarn ios and then you』ll wait until the app magically opens on your desired device. But what happens while you wait?

As soon as you type in one of those commands (which are react-native run-android and react native run-ios, respectively) you start up the packager. One such packager is Metro. The packager takes all your JS code and puts it into a single file: main.bundle.js. When your app finally opens on your phone, the phone will look in the place that』s familiar to it: either the android or the ios directory. That』s the native entry point that I mentioned above. This native entry point will start the JavaScript virtual machine in a thread. The bundled code that is contained in main.bundle.js will then be run on this thread.

The code that』s now running within this JavaScript VM thread will communicate with the native thread using the React Native bridge.

Before jumping to the RN bridge and then analyzing the performance of RN apps, let』s understand how your phone is able to run JavaScript code.

JavaScriptCore

JavaScriptCore is a framework that allows JavaScript code to be run on mobile devices, for instance. On iOS devices, this framework is directly provided by the OS. Android devices don』t have the framework, so React Native bundles it along with the app itself. This increases the app size just a little bit, but it』ll barely matter in the end.

There』s one thing I want to point out that might save you some debugging time. JavaScriptCore is used for running JS code when the app is run on a device. However, if you choose to debug your app, the JS code is going to run inside Chrome. Chrome uses the V8 engine and uses WebSockets for communicating with the native code, so you』ll be able to see important info such as properly formatted logs and what network requests are being made. Just remember that there are differences between the V8 engine and JavaScriptCore — they are different environments and you might run into bugs that only happen when the debugger is attached but not when your app is run normally on your device!

The React Native Bridge

The RN bridge is written in Java/C++ and it allows the communication between the main thread of your app and the JavaScript thread. It uses a custom message-passing protocol to allow this communication to happen.

The JavaScript thread will decide what must be rendered on the screen. It will say to the main thread, 「Hey, I need you to render a button and a text. Thanks.」 It will use the bridge to say this. The message will be sent as a serialized JSON. But apart from what needs to be rendered on the screen, the message must also state where it will be rendered. Here』s where the shadow thread comes into play. The shadow thread is launched along with the JavaScript thread, and it helps to compute the positions of the views. The results are passed along in the aforementioned message, sent by means of the bridge to the main thread.

Any action that the user does on the UI will happen on the main thread. Tapping on a button, toggling a switch — any action must be serialized and sent by means of the bridge to the JavaScript thread. There』s where all the logic of your app happens.

How Performance Is Affected

Let』s review what we have covered so far. The user taps on a button. This action is understood by the main thread and passed as a message to the JavaScript thread. Here, some logic is handled, then the UI must change accordingly. The shadow thread decides where these changes happen, then the updates are sent as a message back to the native thread. Since the user won』t tap too rapidly on the screen, we won』t generally have any performance issues in normal usage scenarios — the bridge handles the communication pretty quickly.

What is cool about React Native (in comparison to other platforms such as Cordova) is that it doesn』t run its code inside of a WebView. It uses native views.

This advantage means that we』ll be able to develop smooth and fast apps that can run at 60 FPS. If you modify the state of a component that is very high in the tree (and you didn』t dedicate too much time to prevent useless re-renders), then the whole component tree will be re-rendered. This won』t be visible to the user in most cases. However, if those descendants are computationally expensive, then you』ll notice your app stuttering for a little bit.

In a nutshell:

When you run a RN app, all you JavaScript code is bundled together into a package called the JS Bundle. The Native Code is kept separately.

The Execution of React Native apps happens over three threads:

  • The JavaScript thread: used by the JS Engine, to run the JS Bundle
  • The Native/UI thread: used to run the Native Modules and to handle operations like UI Rendering, user gesture events etc.
  • Additionally there is a 3rd thread called the shadow thread, which is used to calculate the Layout of Elements before rendering them on the host screen

The Communication between the JS and Native Threads is carried over an entity called the bridge. When sending data through the bridge it has to be batched(optimized) and serialized as JSON. This bridge can only handle asynchronous communication.

img

Old Architecture in React native

JavaScriptCore: It is the name of a JavaScript Engine, which is used by React Native to execute JS code.

Yoga: It is the name of a Layout engine, which is used to calculate positions of UI elements for the user』s screen.

JavaScript Interface (JSI)

In the old architecture, React Native uses the Bridge Module to make communication possible between the JS and Native threads. Every time data is sent across the bridge, it has to be serialized as JSON. When the data is received on the other side it must be decoded as well.

This means that the JavaScript and Native worlds are unaware of each other (ie. the JS thread cannot directly call a method on the Native thread)

Another important point to note is; messages send over the bridge are asynchronous in nature, which is a good thing for most use cases, but there are certain instances when JS code and native code needs to be in sync.

Let』s take an example to better understand the bridge:

If the JavaScript thread needs access to some native modules (eg. Bluetooth), it will need to send a message to the native thread. The JS thread will send a serialized JSON message to the bridge. The bridge will optimize this message and send it over to the native thread. The message will be decoded on the native thread, and eventually the required native code will be executed.

img

  • JS thread prepares message for the Native Thread
  • It is serizlized as JSON before sending across the bridge
  • It is decoded when recieved on the other end of the bridge
  • Then the native thread executes the required native code

New Architecture

However, in the New Architecture, the bridge is going to be replaced with a module called JavaScript Interface\, which is a lightweight, general-purpose layer, written in C++ that can be used by the JavaScript engine to directly invoke/call methods in the native realm.

What does general-purpose mean?

The current architecture uses the JavaScriptCore Engine. The bridge is only compatible with this particular engine. However, this is not the case for JSI. The JavaScript Interface will be decoupled from the Engine, which means that the new architecture enables the use of other JavaScript Engines like Chakra, v8, Hermes etc. Hence the term 「general-purpose」.

How can JSI enable JavaScript to directly call native methods?

Through the JSI, Native methods will be exposed to JavaScript via C++ Host Objects. JavaScript can hold a reference to these objects. And can invoke the methods directly using that reference. This is similar to the web, where JavaScript code can hold a reference to any DOM element, and call methods on it. For Example: when you write:

const container = document.createElement(『div』);

Here, the container is a JavaScript variable, but it holds a reference to a DOM element which was probably initialized in C++. If we call any method on the 「container」 variable, it will in turn call the method on the DOM element. The JSI will work similarly.

Unlike the bridge, the JSI will allow JavaScript code to hold a reference to Native Modules. And through the JSI, JavaScript can call methods on this reference directly.

img

  1. JavaScript has a direct reference to a native module
  2. It calls a method on this native module, via the JavaScript Interface.

To Sum it up, JSI will enable the use of other JavaScript Engines & it will allow for complete interoperability between the threads, the JavaScript code could communicate with the native side directly from the JS thread. This will eliminate the need to serialize JSON messages and will fix the congestion and asynchronous issues on the bridge.

Another big advantage of the JSI is that it is written in C++. With the power of C++ React Native can target large number of Systems like Smart TVs, Watches etc.

Fabric

Fabric is the rendering system, which will replace the current UI Manager.

To understand the advantages of Fabric, first let』s look at how UI is currently rendered in React Native:

When your app is run, React executes your code and creates a ReactElementTree in JavaScript. Based on this tree, the Renderer creates a ReactShadowTree in C++.

This shadow Tree is used by the Layout Engine to calculate the positions of UI elements for the host screen. Once the results of the Layout calculation are available, the shadow tree is transformed into HostViewTree, which comprises of Native Elements. (For example The ReactNative element will be translated into ViewGroup in Android & UIView in iOS respectively).

img

ReactElementTree (JavaScript) -> ReactShadowTree(C++) -> HostViewTree(Native)

Problems with this approach:

As we know, all the communication between threads happens over the bridge. Which means slow transfer rates, and unnecessary copying of data.

For Example: If a ReactElementTree Node happens to be an , then the consequent node of the ReactShadowTree will also be an image. However, this data will have to be duplicated and stored separately in both the nodes.

That』s not all. Since the JS and UI threads are not in sync, there are certain use cases when your app can seem laggy as it drops frames. (Example: Scrolling through a FlatList with a huge list of data)

What is Fabric?

According to the official ReactNative documentation,

「Fabric is React Native』s new rendering system, a conceptual evolution of the legacy render system」

As we have seen in the JSI section of this article, the JavaScript Interface will directly expose native methods to JavaScript, which also includes UI methods. As a result of this, the JS and UI threads can be in sync. This will improve performance for lists, navigation, gesture handling etc.

What are the benefits of Fabric?

With the new rendering system, user interactions such as scrolling, gestures, etc can be prioritized to be executed synchronously in the main thread or native thread. While other tasks such as API requests will be executed asynchronously.

That』s not all. The new Shadow Tree will be immutable, and it will be shared between the JS and UI threads, to allow straight interaction from both ends.

As we have seen, in the old architecture React Native has to maintain two hierarchies/DOM nodes. But since the shadow tree will now be shared among realms, it will help with reducing memory consumption as well.

3. Turbo Modules

In the old architecture, all the Native Modules used by JavaScript (e.g. Bluetooth, Geo Location, File Storage, etc) have to be initialized before the app is opened. This means, that even if the user doesn』t require a particular module, it still has to be initialized at start-up.

Turbo Modules are an enhancement over these old Native modules. As we have seen in the previous part of this article, now JavaScript will be able to hold a reference to these modules, which will allow JS Code to load each module only when it is required. This will significantly improve start-up time for React Native apps.

4. CodeGen

All this talk of Turbo Modules and Fabric sounds promising, but JavaScript is a Dynamically typed language, and JSI is written in C++, which is a Statically Typed Language. Consequently, there is a need to ensure smooth communication between the two.

That』s why the new architecture will also include a static type checker called CodeGen.

By using the typed JavaScript as the source of truth CodeGen will define interface elements used by Turbo Modules and Fabric. It will also generate more native code at build time, instead of run time.

Summary

If we combine all the changes, the new architecture will look like this:

img

Here the key highlights are:

• Bridge will be replaced by JSI

• Ability to swap the JavaScriptCore with other Engines

• Complete Interoperability between all threads

  • Web-like Rendering system
  • Time-sensitive tasks can be executed synchronously
  • Lazy Loading of Turbo Modules
  • Static Type Checking for compatibility between JS and Native Side.

this new structure will give React Native some powerful improvements.


下載開源日報APP:https://openingsource.org/2579/
加入我們:https://openingsource.org/about/join/
關注我們:https://openingsource.org/about/love/