React Native - when JS is too busy
Tags: reactnative, performance
Published: 2021-09-14 18:42:45

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

![5myqp4.jpg](https://cdn.hashnode.com/res/hashnode/image/upload/v1631644871912/ge7HbErcx.jpeg)

I'll try to keep it simple.<br/>
Today React-Native works using 3 main threads

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1630056713222/F7y_onXik.png)
 [source](https://litslink.com/blog/new-react-native-architecture) 


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

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

1. **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](https://cdn.hashnode.com/res/hashnode/image/upload/v1630057876395/qeAScdsxLS.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](https://cdn.hashnode.com/res/hashnode/image/upload/v1630058342755/CcRDtzJqS.png)

🕵️ to spy on the bridge, include this code somewhere

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

what does the bridge data look like?
this is a single event sent from native to JS
```json
{
  "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 non responsive 🥶
from  [The Ultimate Guide to React Native Optimization](https://callstack.com/blog/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 wasn’t performed causing jitters in the UI.

💥  _**[...] when your bridge is busy processing the data**_  👈
this is the point we're interested in. JS runtime has to handle all incoming messages from the bridge and also it has to run the JS app code.<br/>
**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 time

![nodejs-event-loop.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1631278465229/wZTbzjKmW.png)
It tries to do its best by executing all code at the best performance: the event loop checks 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 the next code to execute from the stack! That’s why you’re feeling like you’re executing code in parallel.

When the execution of a block of code is completed, 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 that 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! Here’s 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.

Here’s an example where the user interacts with the UI and the interaction is normal and smooth.

```typescript
const initialCounter = 1;
export const Counter = () => {
  const [counter, setCounter] = useState(initialCounter);
  return (
    <Content>
      <Button onPress={() => setCounter(c => c + 1)}>
        <Text>Increase counter</Text>
      </Button>
      <Text>{`counter: ${counter}`}</Text>
      <Button
        primary={true}
        bordered={true}
        onPress={heavyCode}
      >
        <Text>🔥🔥 run heavy code</Text>
      </Button>

      <Button
        primary={true}
        bordered={true}
        onPress={() => setCounter(initialCounter)}
      >
        <Text>reset</Text>
      </Button>
      <Slider minimumValue={0} maximumValue={10} style={{ width: 200 }} />
    </Content>
  );
};
``` 
{% youtube kpQ5Cfr4-9A %}

Now let's introduce some code that blocks JS runtime so that any other tasks can't executed
```typescript
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>{`counter: ${counter}`}</Text>

      <Button
        primary={true}
        bordered={true}
        onPress={heavyCode}
      >
        <Text>🔥🔥 run heavy code</Text>
      </Button>
      <Button
        primary={true}
        bordered={true}
        onPress={() => setCounter(initialCounter)}
      >
        <Text>reset</Text>
      </Button>
      <Slider minimumValue={0} maximumValue={10} style={{ width: 200 }} />
    </Content>
  );
};
``` 
{% youtube QAxJPt0RdpQ %}

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 forming 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

```typescript
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](https://github.com/mrousavy/react-native-multithreading) 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](https://reactnative.dev/docs/timers#interactionmanager) 
- 🤮 give your code some time to breathe. In this case runtime can schedule other tasks, like handling inputs from native

```typescript
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" tasks could flood the JS runtime)
- unnecessary re-renders
- not optimized / bad code 


I hope you enjoy this article 💪

here’s 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
```