Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
September 14, 2021 06:42 pm GMT

React Native - when JS is too busy

Let's see together how few lines of code could make your app unresponsive. You should always keep in mind that, despite your are writing JS code, your are still executing your code in a device with limited resources.

5myqp4.jpg

I'll try to keep it simple.

Today React-Native works using 3 main threads

image.png
source

  1. The JavaScript Thread. This is the place where the entire JavaScript code is placed and compiled

  2. The Native Thread. This is the place where the native code is executed.

  3. The Shadow Thread. It is the place where the layout of your application is calculated

So it's pretty clear that the native side and the JS side have to communicate. This is done using the bridge: JS invokes native methods and receives back - through the bridge - method results and events coming from the user interaction

bridge.png

let's do an example: the user presses a button.

  • native code handles the onPress event
    • pack the payload to send over the bridge
    • send the payload
  • JS code
    • unpack the received payload
    • execute the bound code

event.png

to spy the bridge, include this code somewhere

import MessageQueue from "react-native/Libraries/BatchedBridge/MessageQueue";const spyFunction = (spyData: SpyData) => {    console.log(spyData);};MessageQueue.spy(spyFunction);

how does the bridge data look like?
this is a single event sent from native to JS

{  "type": 0,  "module": "RCTEventEmitter",  "method": "receiveTouches",  "args": [    "topTouchStart",    [      {        "target": 363,        "pageY": 552.3333282470703,        "locationX": 11.666666030883789,        "locationY": 16.666662216186523,        "identifier": 1,        "pageX": 198,        "timestamp": 12629700.739797002      }    ],    [      0    ]  ]}

Our app is going to be not responsive

from The Ultimate Guide to React Native Optimization

the number of JavaScript calls that arrive over to the bridge is not deterministic and can vary over time, depending on the number of interactions that you do within your application. Additionally, each call takes time, as the JavaScript arguments need to be stringified into JSON, which is the established format that can be understood by these two realms.
For example, when your bridge is busy processing the data, another call will have to block and wait. If that interaction was related to gestures and animations, it is very likely that you have a dropped frame the certain operation wasnt performed causing jitters in the UI.

[...] when your bridge is busy processing the data
this is the point we're interested. JS runtime has to handle all incoming messages from the bridge and also it has to run the JS app code.

So what if we write such a bad code that our JS runtime will be stuck busy for a long time?
can it also handle the events coming from the native side?
no! it can't

JavaScript single-threaded model

JavaScript is a single-threaded programming language. In other words, JavaScript can do only one thing at a single point in ti

nodejs-event-loop.png
It tries to do its best by executing all code at the best performance: the event loop check if there is some code to execute (in the call stack) and execute it!

When you write a code that can finish in the future like the setTimeout() function or make a fetch request (executed in the native side), the event loop - since it has to wait - places these futures in the queue and it goes on by picking next code to execute from the stack! Thats why youre feeling like youre executing code in parallel.

When the execution of a block of code is complete, the event loop checks on the queue if there are some future results or callbacks ready and it adds them to the execution stack

make the app unresponsive

You just need to pay attention to how you write your code and avoid anything that could block the thread, like synchronous network calls or infinite loops.

In the previous paragraphs we learnt that the JS runtime is only capable of executing one task at a time. The execution of the current task blocks the other ones are waiting until it is finished. We learnt that a code that could finish in the future (network call or some methods executed in the native side) is placed in a "waiting" queue and checked every time the run time is "free": if the result is ready, then it will be executed.

We also learnt that the JS runtime has to handle all messages coming from the native side. So, starting from the beginning of this article

Can a piece of code freeze our app?

yes! Heres why

If we write a very expensive block of code, we'll keep event loop busy for a while (depends how much time the task takes). In that timespan JS isn't able to process other events, like those coming from the native side.

Heres an example where the user interacts with the UI and the interaction is normal, smooth.

const initialCounter = 1;export const Counter = () => {  const [counter, setCounter] = useState(initialCounter);  return (    <Content contentContainerStyle={styles.contentContainerStyle}>      <Button onPress={() => setCounter(c => c + 1)}>        <Text>Increase counter</Text>      </Button>      <Text style={{ marginTop: 4 }}>{`counter: ${counter}`}</Text>      <Button        onPress={() => setCounter(initialCounter)}      >        <Text>reset</Text>      </Button>    </Content>  );};

Now let's introduce some code that blocks JS runtime so that any other tasks can't executed

const heavyCode = () => {  let n = 100000000;  while (n > 0) {    n--;  }};const initialCounter = 1;export const Counter = () => {  const [counter, setCounter] = useState(initialCounter);  return (    <Content contentContainerStyle={styles.contentContainerStyle}>      <Button onPress={() => setCounter(c => c + 1)}>        <Text>Increase counter</Text>      </Button>      <Text style={{ marginTop: 4 }}>{`counter: ${counter}`}</Text>      <Button        onPress={heavyCode}      >        <Text> run heavy code</Text>      </Button>      <Button        onPress={() => setCounter(initialCounter)}      >        <Text>reset</Text>      </Button>    </Content>  );};

As you can see we pressed the button but the UI updates after 3-4 seconds

This is because the JS runtime is busy executing our bad code. While it is busy the user presses the button multiple times, the Native side sends those events through the bridge, but they are queued and they cannot be evaluated by JS until it ends the execution of this blocking task.
This is the reason why you see the counter updates come in a row after a few seconds.

Can a Promise solve or reduce the problem? no.
Executing some blocking code inside of a Promise is the same as executing it and at the end of callback

const heavyCodeAsync = (): Promise<void> =>  new Promise(resolve => {    heavyCode();    resolve();  });

TL;DR

If you run a blocking code in your JS/TS your app will be stuck or laggy: the interaction inputs won't be processed at the expected time.

How can I avoid it?

  • don't write blocking code
  • if you can't avoid that code, process it in the native side and give back only the results
  • take advantage of the next coming React Native architecture JSI + Fabric Rendering + Turbo Modules
  • run your code at the right time, if it's possible InteractionManager.runAfterInteractions
  • give your code some time to breathe. In this case runtime can schedule other task, like handling inputs from native
const breath = (): Promise<void> =>  new Promise(resolve => {    setTimeout(resolve, 20);  });const heavyCode = async () => {  let n = 100000000;  while (n > 0) {    n--;    await breath();  }};

Which kind of scenario can cause this pitfall?

  • making heavy computation (i.e: ordering large arrays, crypto, strong math operation, image processing, etc..)
  • spawn a lot of Promise (a lot of "async" task could flood the JS runtime)
  • unnecessary re-renders
  • not optimized / bad code

I hope you enjoy this article

heres my environment where I ran the examples. The JS runtime is hermes on iOS

System:    OS: macOS 11.4    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz    Memory: 219.67 MB / 32.00 GB    Shell: 5.8 - /bin/zsh  Binaries:    Node: 12.13.0 - ~/.nodenv/versions/12.13.0/bin/node    Yarn: 1.22.11 - /usr/local/bin/yarn    npm: 6.12.0 - ~/.nodenv/versions/12.13.0/bin/npm    Watchman: 2021.09.06.00 - /usr/local/bin/watchman  Managers:    CocoaPods: 1.10.1 - /usr/local/bin/pod  SDKs:    iOS SDK:      Platforms: iOS 14.5, DriverKit 20.4, macOS 11.3, tvOS 14.5, watchOS 7.4    Android SDK: Not Found  IDEs:    Android Studio: 4.2 AI-202.7660.26.42.7486908    Xcode: 12.5.1/12E507 - /usr/bin/xcodebuild  Languages:    Python: 2.7.16 - /usr/bin/python  npmPackages:    @react-native-community/cli: Not Found    react: 17.0.1 => 17.0.1     react-native: 0.64.2 => 0.64.2   npmGlobalPackages:    *react-native*: Not Found

Original Link: https://dev.to/matteoboschi/react-native-when-js-is-too-busy-5fhn

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To