Optimising React Apps
Tags: react, performance, webdev, javascript
Published: 2024-04-21 19:10:41



In this article, we try to understand the approach to optimising React applications. We'll work with a real example to explore some of the most common issues that developers face. Then, we'll go through the process of improving our application to tackle those issues effectively. We'll also learn how to make informed decisions and strike the right balance between different possible options.

> Note: This is a lengthy article. However, you may already be familiar with some of the topics we discuss. Therefore, I encourage you to selectively read the parts that are relevant to you.

## Table of contents
{%- # TOC start (generated with https://github.com/derlin/bitdowntoc) -%}

- [Table of contents](#table-of-contents)
- [Introduction](#introduction)
- [Performance challenges with React](#performance-challenges-with-react)
   * [Scheduling an update](#scheduling-an-update)
   * [Render phase](#render-phase)
   * [Commit phase](#commit-phase)
   * [Where is the issue?](#where-is-the-issue)
   * [How do we optimise?](#how-do-we-optimise)
- [Understanding with a real example](#understanding-with-a-real-example)
   * [Monitoring the application](#monitoring-the-application)
- [No optimisation](#no-optimisation)
   * [Profiling](#profiling)
      + [T = 0 - 18s ](#t-0-18s)
      + [T = 18s - 33s](#t-18s-33s)
      + [T = 33s - 60s](#t-33s-60s)
   * [Adding optimisation](#adding-optimisation)
      + [Trade-off](#tradeoff)
- [Add memoization](#add-memoization)
   * [Profiling](#profiling-1)
   * [Adding optimisation](#adding-optimisation-1)
      + [Trade-off](#tradeoff-1)
- [Reduce renders](#reduce-renders)
   * [Profiling](#profiling-2)
   * [Adding optimisation](#adding-optimisation)
      + [Trade-off](#tradeoff-2)
- [Reduce computation](#reduce-computation)
   * [Profiling](#profiling-3)
   * [Adding optimisation](#adding-optimisation-2)
      + [Trade-off](#tradeoff-3)
- [Optimise Updates](#optimise-updates)
   * [Understanding how the store is structured](#understanding-how-the-store-is-structured)
   * [Profiling](#profiling-4)
   * [Adding optimisation](#adding-optimisation-3)
      + [Trade-off](#tradeoff-4)
- [Optimise Memory](#optimise-memory)
   * [Profiling](#profiling-5)
   * [What went wrong?](#what-went-wrong)
- [Summing up](#summing-up)
      + [Credits](#credits)

{%- # TOC end -%}

## Introduction
React has become increasingly popular in recent years for building user applications due to its introduction of declarative programming. Previously, developers had to write code to update different parts of the UI whenever there was a change. This process was difficult to manage at scale and often resulted in bugs as the application evolved. React changed that by defining the UI as a function of state. Now, developers only need to focus on updating the state correctly, and React takes care of updating the UI.

![Stack overflow trend for questions asked for frontend technologies over the years](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sg6byphbxt2mpjaixg3c.png)

React is a good tool for building correct UIs quickly. However, the next problem down the line is performance.

## Performance challenges with React
React is not without its limitations. Over the years, it has become increasingly clear that it is difficult to create performant React applications. Let us go over how React updates UI to understand where the problem lies.

### Scheduling an update
React maintains a tree of your components. These components can have some [state](https://react.dev/learn/state-a-components-memory). Developers can request an update to the state using `setState`. This instructs React to schedule an update for the component's state. React performs [render and commit](https://react.dev/learn/render-and-commit) to process the update for the subtree under this component.

### Render phase
The component is re-evaluated using the new updated state. For function components, this means invoking the function again. React recursively re-evaluates all the children of the component being updated. In the end, React has the output of all components in the tree. React requires this to be done using pure functions which means if rendering is done multiple times with the same state, output should be the same. The output should only depend on the input and no other external factor. Components can describe any side effects that need to run separately using [useEffect](https://react.dev/learn/synchronizing-with-effects).

### Commit phase
The output of the render phase is an updated representation of  DOM under the component subtree (often called Virtual DOM). React now has the outdated DOM for the subtree along with the updated DOM. It has a `O(n)` [algorithm](https://legacy.reactjs.org/docs/reconciliation.html) to patch the outdated DOM and update it.

### Where is the issue?
The commit phase is very straightforward. Under the imperative paradigm, developers would manually update the DOM which resulted in inefficient updates. React replaced that by carefully only updating what is needed in one go. 

The render phase is usually more computationally expensive since the entire subtree is calculated again. That in itself is not a big problem for a single update. However, when you have multiple updates occurring rapidly, it becomes a real problem. The React team has [acknowledged](https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024#react-compiler) that and has started working towards mitigating it.

![React can re-render too much](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5lar8hrcgq8652cz1odh.png)

Imagine you have a parent component with 3 state variables - `s1`, `s2` and `s3`. It also has 3 children which rely on one of these variables.

![Example of the component tree](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fyy5l28yuz1y4pjluwu3.png)

Even if only `s1` is changed, the parent and all the children are re-rendered instead of just parent and Child 1. This is fine since the rendering process is pure and re-rendering other children will return the same result. However, when a lot of updates are made to `s1`, `s2` and `s3` rapidly, the performance overhead becomes evident. This is often the case with highly interactive apps (like chat, video conferencing etc).

React has introduced measures like batching state updates, background concurrent rendering and memoization to tackle this. My opinion is that the best way to solve the problem is by improving their reactivity model. The app needs to be able to track the code that should be re-run on updating a given state variable and specifically update the UI corresponding to this update. Tools like [solid.js](https://www.solidjs.com/) and [svelte](https://svelte.dev/)  work in this manner. It also eliminates the need for a virtual DOM and diffing.

### How do we optimise?

Software optimisation is usually not a simple process. Each change has trade-offs associated with it. Some trade-offs might be acceptable for your use case while others might not be acceptable. This makes optimisation a very specific process. In general, it is repeated iterations of the following steps:

1. Observe the application under stress.
2. Find which parts of the app run into performance issues.
3. Debug the root cause of these performance issues.
4. Come up with approaches to mitigate those.
5. Compare trade-offs between these approaches and select one most suitable to your use case.

## Understanding with a real example

Let us consider a stock monitoring application. This mock app monitors rapidly updating stock prices in real-time. It is deployed [here](https://react-performance-omega.vercel.app/). For the best experience, you should clone and run the app from the source from [here](https://github.com/yashmahalwal/react-performance).

![App screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gpuqftjpmc7p3hipw5yz.png)


This app has the following sections:
1. Header with navigation to different versions of stock monitoring  ![Header of the app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ft6l3xc184ryg1z5vysx.png)

3. Buttons to observe/unobserve the stocks and reset the stock data ![Actions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r3m9wqo2edaocaylyt0u.png)

4. A plot of the running average of the last 50 entries ![Plot of running average](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmi91ret2khj56svhwhv.png)
5. A list of stock price events - contains the name of the stock and the current price ![Table of stock events](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/14id35qbrjat3qllr3gr.png)

6. A profiler widget which:

- Shows how many renders take place **every second** and the average render & commit durations of those updates.

- Can plot line chart for the number of renders, average render duration and total time spent calculating per second throughout profiling.

```
    Total time = (Render + Commit) * Render Count
```

![Profiler Widget](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cffiys44wt24mekszxqg.png)

To keep things consistent, I will profile each version of the app for **1 minute**. Stock events are fired at every `200ms`. We will observe how the app behaves and iterate over the code to make it perform better.

### Monitoring the application

The app already has a profiler widget built on the [Profiler API](https://react.dev/reference/react/Profiler). The development build of the app is deployed so that monitoring can be done with ease.

In addition, you can use React developer tools to see component trees and profile them. This will provide a breakup of why each re-render took place and how much time each component took to re-render.

![Developer tools for monitoring performance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifejqitvjc0zjoptekgg.png)

Chrome also has a built-in performance monitor which helps you see the overall behaviour of the application outside React's context

![Chrome performance monitor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jx99esor229mblezzk4e.png)


## No optimisation
Let us observe the app without any optimisation. To see the performance fluctuations better, you can add an artificial CPU slowdown. I will be working with a `6x` slowdown.

![CPU Throttling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vb6zdu15jo95g2el9z3d.png)

Go ahead and click on observe. Remember to start profiling before you start observing. When you want to stop, click on `Unobserve`.

![App demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zbwxy8uvtdq3mpnpypft.gif)

### Profiling

Let us look at the plots generated by the profiling widget and understand the trends:

![Render Count](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pqwseff0m3zu01igd681.png)


![Render Duration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uoh6xc3rzedctxvky7u3.png)


![Total time spent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5oooud8x3077s9ilrzic.png)


#### T = 0 - 18s 
- Number of renders starts with `10-11` renders per second. It stays at that for a while.
- Render duration increases linearly from `0` to `50ms`
- Total time spent increases linearly from `0` to `500ms`. 

These trends show that with time, the load on the JS thread increases. Render duration increases which also increases the total time spent. Also, `500ms = 10 x 50ms` which means that the JS thread can process all the renders so far.

#### T = 18s - 33s
- The number of renders fall rapidly from `10` to `5`
- Render duration keeps on increasing almost linearly to `110ms`
- Total time spent oscillates between `500ms` to `650ms`.

If you look closely, these trends are due to the rendering duration increasing over time. This blocks the `JS` thread for more time and leaves less available time for other renders per second. Total time spent shows the correlation between decreasing render count and increasing render duration. When `JS` thread throttles and reduces the number of renders processed, the total time spent is lesser. However, since the average render duration also increases, the total time spent rendering increases again. 

#### T = 33s - 60s
- Number of renders continues to fall. It falls from `5` to an average of `3-4` renders towards the end. The decrease is not as sharp as before
- Render duration still keeps on increasing. However, now the growth has slowed down. This is why the render count did not fall as sharply as before.
- Total time keeps on oscillating. The average is still `500-600ms` with large fluctuations on either side.

This is simply because the JS thread is close to reaching the throughput limit and hence, the number of renders processed is reduced. Growth in calculations becomes slow. The app becomes laggy and less responsive at this point.

Here is a screenshot of the performance analysis using Chrome  Inspector. 

![Chrome Inspector](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g1eoxk4uko38w799wtn7.png)

If you look at the top, there are a lot of lagging frames. These updates are displayed but they lag behind the main thread. Those are coloured yellow and labelled as [partially presented frames](https://developer.chrome.com/blog/new-in-devtools-100#perf). JS thread is busy for `92%` of the observation time. 

Now let us take a look at the React profiler under Chrome Inspector.

![Hook 1 changed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dqagftulgt3sgj254jvu.png)


![Hook 2 changed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y84p93s1q07cv93kryt5.png)

This further tells us two things:
1. The `Table` component is the most expensive to render
2. During the renders, hooks `1` and `2` of the `Unoptimised` component change alternatively.

### Adding optimisation

We are familiar with how the app behaves. The table component is the bottleneck. We can start by reducing the time spent rendering it. Let us look at the unoptimised version of the app (`src/routes/unoptimised/unoptimised.tsx`).


![Hooks for the unoptimised component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jy8wra3ow49hzto2pdmr.png)

These are the hooks that React Profiler reported:

![Hooks causing re-renders](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vf5olbph1zpxb1z0g81f.png)

1. `stockEventList` is used by the table.
2. `averagePrices` is used by the line chart plot.

These are updated alternatively due to useEffect on `line 35`. Since they are used to maintain separate parts of the UI, we can add memoization on the children to ensure they only re-render the component they are consumed by. So updating `averagePrices` should not re-render the table and updating `stockEventList` should not re-render the line chart.

#### Trade-off
This change will reduce the render durations. Memoization will increase the memory usage. React docs say that it [won't be harmful](https://react.dev/reference/react/useMemo#should-you-add-usememo-everywhere). 

It will also introduce refactoring. As a developer, you try to make minimal changes to the codebase. This ensures that existing logic does not break. **However, with React, you'd often be required to prune and refactor big components into smaller ones. This is because your functions map directly to a part of the UI.**  Solid.js takes an interesting approach to [components](https://www.solidjs.com/tutorial/introduction_components) as simply a means to modularity and reusability. Breaking into components does not impact how much code runs on updates.

## Add memoization
The final code can be seen under (`src/routes/add-memo/add-memo.tsx`). Here is a list of changes made:

1. The table and line chart components are wrapped in `useMemo` to ensure that they are not re-rendered when non-relevant changes cause re-rendering of the application.
2. Moved inline constants outside the function component. This ensures that we are not creating new but equivalent objects on re-render as these might cause re-render of the children.

React is planning on moving to a [new compiler](https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024) which can do some of this for you. But right now, this responsibility falls on the developer.

### Profiling
Let us look at the profiling plots again:

![Render Count](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m8umb2kk25qnhp34i31v.png)


![Average render duration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yymm6trq6s8wlq0dqrha.png)


![Total time spent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ne60sbmo2nycpe7j3j0t.png)

There are some interesting trends this time: 

1. The average render duration is reduced. It has become half from a maximum of `180ms` to `90ms`. This is because the table is not re-rendered on every alternate render.
2. This is reflected in the JS thread being able to process renders without any issues up to `39s` which is a huge improvement over the previous `18s`.
3. Render count is sustained at `10` up to `39s` and then starts to fall.
4. Total time spent increases up to `39s` and then fluctuates around the previous range.  

Based on these and the insights from the unoptimised version, we understand that the JS thread starts struggling when the total time spent reaches `500-600ms`. In the long run, this version of the app will also run into the same issues as before. It will take longer but the app will ultimately become less responsive.

Now let us look at the React Profiler.


![Table re-rendered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jqdsdhc0hjlooxzdfhod.png)




![Chart re-rendered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g45hd49wm9qf6evl97d3.png)


The change in trend is because every alternate render is smaller. However, the render duration for the table keeps on increasing.

### Adding optimisation

If stock events are being sent every `200ms`, there should be `5` renders every second. However, we see that the renders (when the app is capable of handling the load) are twice that number. Let us focus on reducing the number of renders further here. If you look at the code (`src/routes/add-memo/add-memo.tsx`), you'll see that there is a `useEffect` on `line 51`.

![useEffect causing re-renders](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9layy7hw4je19o8uq65.png)

This `useEffect` runs when the list of stock events is changed. It calculates the average of the last 50 entries and also scrolls the table to the bottom. However, `useEffect` is meant for synchronising with external systems. This is important due to the timing of triggering `useEffect`. It runs [after rendering](https://react.dev/reference/react/useEffect#caveats), in a deferred event. If it runs React-related code, there might be a [better place](https://react.dev/learn/you-might-not-need-an-effect) to put that code. **This is one of the many magical rules that developers must remember and is not strictly enforced by React's design**.

In previous examples, `stockEventList` is updated causing a re-render. This triggers the `useEffect` which updates averagePrices and causes another re-render. We can put the code under useEffect to the observer for stock events. Since multiple state updates are being made together, they will be batched into a single update.

#### Trade-off

This change will again introduce refactoring. Many problems with React code happen because components aren't updated regularly. They can grow too big and confusing, making them hard to manage. Components can be dynamic with `useState` and `useEffect` hooks and it can become easy to lose track of which code inside the component runs when. Some code can run again and again even when not needed due to multiple updates to the component's state.  If that code is run due to an unrelated state update, it causes a functionality bug. It defeats React's purpose of creating correct applications. Developers need to trace the code and describe dependencies for changes. React provides helpful rules which can be enforced by `IDEs` and `linting`. 

Therefore, you should always try to refactor React code while updating it to ensure that only the correct code runs for a minimal number of times. Currently, this responsibility lies with the developer and introduces extra complexity while writing code.

## Reduce renders
To batch the state updates together, we move the code from `useEffect` into the observer logic. The result can be found here (`src/routes/reduce-renders/reduce-renders.tsx`)

![Move code to observer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vd5p28l2tzpbmklo8omw.png)

Another change you might notice is `useStableCallback` hook. It is a hook that creates a function that does not change in reference but always runs the latest code when invoked. It is inspired by class components. React is planning on bringing something similar with [useEffectEvent](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).

### Profiling

Both state variables are now updated together in a single render. This is because React has now batched these updates together.

![Hooks change together](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8myezow7g3sxq3u9021j.png)

Let us look at the profiling trends again:

![Render Count](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ox6jln12c9o8503wwcwl.png)


![Render Duration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkxoqssw8z3qf801myb5.png)


![Total time spent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uur45v234t9ooa75sbdw.png)

The app behaves similarly to the previous version. Here are the trends:
1. The number of renders is lower (`5` when the app can process all the updates) which means there are no extra renders. 
2. The average render duration is back to a maximum of `180ms`. 
3. This is because the number of renders is halved but each render is almost twice as expensive. In the previous version, every alternate render was very small. However this time, every update causes the table to re-render. Therefore calculations on every render are the same as the unoptimised version.
4.  Total time spent reaches the `500ms-600ms` range at around the same time mark as before (`39s`).

### Adding optimisation
We have reduced the number of renders.  The app is better than before but it still becomes slower and unresponsive. The root cause is the increasing duration of renders. If you look at the React profiler, you'd realise this is because of the table component

![Table re-renders all the rows](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6qpzwzbs5c1226xd1hi4.png)

The table renders all the rows. On every update, all the rows are re-rendered. As the number of entries increases, the amount of time spent re-rendering all the rows will also increase. The average price plot does not show this trend since we only show running the average of the last 50 entries. It internally uses Canvas which is very performant under rapid updates.

If we limit the number of rows rendered to only show the visible items, we can limit the cost of re-rendering the table. Even if there are a large number of rows in the table, only the ones on the screen will be rendered. That can be done by [virtualization](https://medium.com/@atulbanwar/list-virtualization-in-react-3db491346af4). I will use [react-virtuoso](https://virtuoso.dev/) for the app.

#### Trade-off
This change will make sure that the app doesn't slow down and become unresponsive. However, with virtualization, we sacrifice the smoothness of the scroll. Scrolling rapidly will result in blank flashes inside the table component. This will also happen when the list scrolls to the bottom on new stock events. We also don't render the rows out of view which can hide them from the browser search functionality.  

## Reduce computation
The next version of the app can be found under `src/routes/reduce-computation/components/reduce-computation.tsx`. 

![App with virtualization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n08zp9js3pf35gmm9fkg.gif)

Here are the changes made:
1. The code for the chart and the table is broken into smaller components. They can be separately memoized using `memo`. 
2. Also logic for averages has now moved into the `Chart` component. This prevents the table from being re-rendered if there are updates from the `Chart` component. 
3. We have also eliminated `averagePrices` state and simplified calculations for calculating averages. Earlier we looped over the last `50` entries of the list. Now we create new entries from previous averages.
4. Inside the table component, we have replaced `NextTable` with `TableVirtuoso`

### Profiling
Here are the trends:

![Render Count](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kugx7d09pfgyi3xmxnpc.png)

![Render Duration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uhf6722rbc2iz88gj4rc.png)

![Total time spent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2gwrnj0ff4hfeuba54s.png)

1. The average render duration is much lower. It is between `0.8ms-1.5ms` on average. It does not increase. There are fluctuations which look significant. However, these are just small variations which are magnified due to the small scale of render durations.
2. Total time spent is also limited to `18ms-33ms`. It does not increase with time.
3. The number of renders is constant but much higher with a value of `18-20`. 

To understand why the number of renders is higher, we can check the React profiler.
![Profiler for virtuoso table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3njlqs5mxitextp99vvj.png)

![Which hook re-rendered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/an17zz8y70fttw5oygu4.png)

`Hook 2` causes extra re-renders for the `Virtuoso Table` component. This hook is responsible for tracking which rows are in the view. The component works by showing the visible rows on the screen and adding space above and below them to account for the sizing of the list. We scroll to end on every update which causes re-renders inside the `Virtuoso Table` component.

Here is what the performance analysis looks like:
![Chrome performance profiling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gyjdnudj8cmbcaw304zw.png)

JS thread is occupied for `13.8%` of the observation time which is much lower than the previous `92%`. There are no dropped frames and the app stays responsive in the longer run.

### Adding optimisation
The app still feels unusable. The updates are too rapid for any meaningful interaction. It would be better if we could process multiple stock events together are larger intervals. It would also reduce the load on the JS thread. Also scrolling to the bottom seems to be adding more problems than value to the app. It is very hard to focus on a list that keeps scrolling. It causes extra re-renders in the virtual table and also causes blank flashes while the list scrolls. It can be eliminated if the table shows the list in reverse order, with new entries added to the top. This is something you should discuss with your `UX` team. 

#### Trade-off
Slowing down the rate of updating UI by handling multiple updates together will provide a better `UX` and performance. However, adding multiple events together will also introduce larger visual shifts. It also makes state management complex. There is another overhead of reversing the list while rendering the table. But with slower updates, this should not be significant.

## Optimise Updates
At this point, it is a good step to introduce a [state management library](https://react.dev/learn/scaling-up-with-reducer-and-context). React provides [context and reducers](https://react.dev/learn/scaling-up-with-reducer-and-context) for complex state management. Using them, you can expose your state everywhere in the application and update it as needed. But they have certain limitations:

 1. Context can expose multiple values combined in an object. Even if one of those values changes, the context will cause a re-render everywhere it is being consumed.
 2. You have no control over how the updates are triggered. If there is a re-render in the context provider which changes the context value, then all consumers will re-render.
 3. The developer needs to do most of the heavy lifting around state management logic

Introducing a state management library can help make this process smoother. Here's how:

 1. You can precisely specify the data from the entire state store in your consumer component. The consumer will only re-render when that specific value is changed. So you have fine-grained control over the scope of re-renders on updates.
 2. You can use transient updates. In this case, you can subscribe to a value from the store in a component but you can choose when updates should cause a re-render. This is very useful in applications where there are a lot of rapid updates to the state. It is possible because external stores usually operate outside React's scope and you can control how the [integration](https://react.dev/reference/react/useSyncExternalStore) works with your components.
 3. State management libraries provide a systematic way of maintaining your state-related logic. This is useful when you're working in a large team.
 4. Most of the libraries come with dedicated developer tools. They also come with a plethora of community-maintained plugins which can make complex state manipulation very easy

For our case, points `1` and `2` can help us provide a better way of handling updates in our rapidly updating application. I have used [zustand](https://github.com/pmndrs/zustand) for the application.

### Understanding how the store is structured
The code for the store can be found at `src/routes/optimise-updates/store/stock-store.ts`. Here is how it works:

![Zustand Store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udb44ah721crhipz535k.png)

 1. The store maintains a list of Stock Event IDs. This list can be read in the table component.
 2. A record of Stock events indexed by their ID. Individual table rows can read entries from this record. This is also useful where entries can also be updated. In this case, only the relevant row component will re-render. In the previous version, the table component accepted a list of event objects as `prop`. Changing a single entry would've caused a re-render of the table component.

We also have a custom hook which leverages [transient updates](https://github.com/pmndrs/zustand?tab=readme-ov-file#transient-updates-for-often-occurring-state-changes). We throttle the updates instead of immediately re-rendering the component on state change. The code can be found under `src/routes/optimise-updates/hooks/use-throttled-store.ts`.

![Use throttled store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/puae4dswbqe4ludoehs7.png)

Using this hook will make sure that updates are throttled to the specified interval. The default value is `600ms` which means `3` stock events related updates will trigger `1` re-render.

Now we can use this in the `chart` and `table` component.
![Table with throttled reversed list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j6f0b03h5lmy6cb1bdil.png)
Note that list of IDs is reversed before rendering. This eliminates scrolling to bottom on every event. If needed, reversed list can also be maintained in the store.

![Chart Hook Usage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7x8hbmhgbuuwsrn53gnb.png)

We read the stock event data in `StockTableRow` component. That doesn't need throttling. This is because it gets the ID from the `table` component which already gets throttled updates.

![Stock table row does not need throttling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wio5j0uqi1urhaspig2.png)

![App with throttled updates](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/96mu39dedfx1ik79j9pd.gif)


The final code can be found at `src/routes/optimise-updates/components/optimise-updates.tsx`. 

### Profiling

![Render Count](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxkhhjb8nqtsj64ffv5n.png)


![Render Duration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgmafcnbtnn47xqc8w0a.png)


![Total time spent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mfmnb1tpo5giu6g8sh64.png)

Initially, there was a spike in render count and total time spent. That is due to the virtual table calculating layout as entries cause overflow. However, that is not seen once the scroll has been shown in the table. To avoid the spike, can rely on the fact that all the rows are of the same height. We can ask virtuoso to skip this calculation by [providing the size in props](https://virtuoso.dev/virtuoso-api/interfaces/VirtuosoProps/#fixeditemheight).

After the initial spike:
1. Render count stays between `1-3`
2. Render duration stays between `0.1-0.7ms`
3. Total time spent stays between `0.5-2ms`. 

This is a huge optimisation over the previous version where the total time spent maxed to `33ms`. Performance monitor further shows that UI is less rapidly updated now. JS thread only spends `5.38%` time on rendering effort.
![Performance with optimised updates](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zhn84wxanxllusexfl8z.png)


### Adding optimisation
Now the application seems to be stable. However, if you think about the store, it will grow indefinitely in the long run. That will cause memory usage to grow. We can remove old entries from the store to free up memory. In a real-world use case, you'd refetch those entries when needed again. This will make sure that memory does not increase unbounded.

#### Trade-off
This will add extra complexity to state updates. Processing updates where older entries are deleted might take longer

## Optimise Memory
![Limit memory usage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/seh1jtfvqsbdajogckqg.png)

The updated store can be found here: `src/routes/optimise-memory/store/stock-store.ts`. After the size of the ID list reaches `200`, we remove the first `100` entries from the store. So only a maximum of `200` entries is maintained.

### Profiling
Updated code can be found under `src/routes/optimise-memory/components/optimise-memory.tsx`. Here is the performance analysis before the memory optimisation:

![Performance for optimised updates](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ikl6wrwwc53kio0p7nyq.png)

Here is the performance profile after optimisation:
![Performance with memory optimisation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/024pce9zgswqftvtgzpm.png)

The memory usage doesn't go down by much. Earlier it was in the range of `11.0-19.4 MB`. Now it is in the range of `11.2-19.3 MB`. This is probably because the event objects are too small and don't take much memory. It is also concerning to see a range of dropped frames in between. These are due to the store being cleared which blocks the JS thread.

### What went wrong?
This is an example of over-optimisation. We could save some memory usage but the cost of computation was higher. The version without memory usage optimisation is good enough since there isn't a practical use case where someone would monitor the stocks for so long that their memory is filled up.

But if we want to ensure that their memory does not increase unbounded, we can experiment with the point at which the store is cleared and the number of entries removed at once. Tweaking these parameters should get us into a zone of acceptable performance.

## Summing up
Optimisation is a tricky process. You need to observe your application, correctly debug the bottlenecks and mitigate those with good design. There are added complications due to React's declarative model since you cannot completely control which code runs when. Depending on your use case, there might be other optimisations possible. Some of the common ones are:

 - **Bundle Splitting and Lazy Loading**: Splitting your built application into smaller chunks that can be lazily loaded. These chunks are only sent to the client application if they are needed. It helps reduce the amount of code loaded on the client for your application to work.
 - **Server Side Rendering**: This is a hybrid of the older server rendered applications and newer single-page applications. You can selectively render your components on the server and serve them to the client readily. That helps increase security, abstract away logic from the client and provides better load times & search engine visibility.
 - **Web Workers and Web Assembly**: For applications that are computation-heavy, you can offload the work to a separate thread. These are called web workers. You can also run compiled `C++` (or other supported languages) code via Web assembly which gives you the benefit of high performance in the browser.

Did I miss something? Can I make this article better? Is there something that you liked reading? Let me know in the comments!

#### Credits
Cover photo by RealToughCandy[dot]com: https://www.pexels.com/photo/hand-holding-react-sticker-11035471/