React is not about performance
Tags: webdev, react, performance, javascript
Published: 2024-01-31 20:57:35

Key Takeaways

- React is the most popular JavaScript rendering framework.
- React full-stack frameworks, like Next.JS and Remix, are recommended for usage by the React team.
- React full-stack frameworks cannot work without JavaScript which violates REST principles and affects devices performance.
- HTML duplicated data appears when React Server Components are in use.
- By default React adds extra loading to user devices with unnecessary re-rendering.
- Optimizing React code is a manual procedure and that is error prone.


Facebook presented React to the public in [2013](https://github.com/facebook/react/releases/tag/v0.3.0).

The project immediately gained popularity, simplifying web application development with the next core ideas:  
- UI components are written in the [JSX syntax](https://legacy.reactjs.org/docs/introducing-jsx.html), which is similar to the conventional HTML.
- Usual JavaScript statements (if, switch, &&, etc) and expressions can be used inside UI components declaration. A developer does not need to learn new attributes, like "ngIf" in Angular, "v-if" in Vue.JS, "{#if} {:else if …}" in HandleBars and Svelte.
- The data flow implements the [Flux architecture](https://legacy.reactjs.org/blog/2014/05/06/flux.html) and is unidirectional: changes are populated from top components to their children.


After more than 10 years, React still has the [highest number of downloads](https://npmtrends.com/@angular/core-vs-lit-vs-react-vs-solid-js-vs-svelte-vs-vue) among JavaScript web libraries. It is the [most discussed web framework](https://insights.stackoverflow.com/trends?tags=reactjs%2Cvue.js%2Cangular%2Csvelte%2Cangularjs%2Cvuejs3) in StackOverflow and is used in [3.5% of all websites](https://w3techs.com/technologies/details/js-react#:~:text=React%20is%20used%20by%204.3,is%203.5%25%20of%20all%20websites).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/erfpyh3o2x1zfwvrdpxg.png)

There is, though, a negative point. All this time, React websites produce overhead on client devices.

This is still true, even after introducing such optimizations as improving the React engine speed and page interactions with the [Fiber](https://www.youtube.com/watch?v=ZCuYPiUIONs&t=71s) algorithm, reducing JavaScript file sizes, code splitting, and implementation of [islands](https://deno.com/blog/intro-to-islands) using component streaming and selective hydration.


Moreover, the recent optimizations were done with high competition pressure from other projects ([Angular](url), Svelte, VueJS, Marko), and especially from those either supporting JSX syntax or optimizing the work with the React library (Preact, Qwik, Remix, Astro). 

Let me describe React performance issues from an end-user perspective.

## JavaScript dependency

Representational State Transfer, or REST for short, states that Code-on-Demand (JavaScript) should be [optional](https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) in web pages and applications. This concept is important for many devices working without JavaScript or having low processing power, as well as for search engine website crawlers.

React always supported rendering JSX components to HTML via the [renderToString](https://github.com/facebook/react/releases/tag/v0.12.0) method.  So, it is possible to render a React page on the server and return a response to the devices with a lack of JavaScript. Then, it is feasible to add interactivity to the client side supporting JavaScript with the “render” method (now separated into “renderRoot” and “hydrateRoot”). The problem with that approach is that the whole flow implementation was solely put on developers. This led to the point where many programmers skipped the backend HTML generation and built pure Client JavaScript React applications using the recommended [crate-react-app](https://www.npmjs.com/package/create-react-app) package.


When JavaScript was disabled in a browser, users usually saw a blank page.The reason is that the React NPM package has always been just a library, not a full framework. Only 10 years after the React release, the team added the [recommendation](https://twitter.com/acdlite/status/1617611126514266112) to the official documentation to use one of the frameworks with React Server Side Rendering support, like Next.JS or Remix.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g5r2kjhz3oic5j86yr5n.png)

Though it might look like the JavaScript dependency problem is finally resolved in the development community, this is not the case at the time of writing. React recently introduced the ability to [stream the page content](https://react.dev/reference/react-dom/server/renderToPipeableStream) from the server to a client’s browser in parts, and it requires JavaScript to work.


The added streaming optimization resolves the next problem. Some components require data to be fetched from external resources like a database, and it delays rendering. The solution, introduced in [MarkoJS](https://dev.to/ryansolid/server-rendering-in-javascript-optimizing-performance-1jnk) in 2014, is to separate “immediately” rendered components, with static data, from “suspended” components waiting for dynamic data from external resources. The page layout with “immediate” components will be streamed to the browser, while placeholders (usually with spinners or skeletons) will be preserved for “suspended” components. 

```
<main>
  <aside>
    <!--Sidebar -->
   </aside>

  <!-- Suspense PLACEHOLDER-->
  <div id="post_placeholder">
    <!-- Spinner -->
  </div>
</main>
```

Once a “suspended” component is ready, it is streamed back to the client in the same page request (long HTTP connection).

How is it possible to post-stream and update HTML in the middle of the page without JavaScript? Currently, it is not supported. React sends HTML code for the suspended component with a small [script code](https://github.com/reactwg/react-18/discussions/37) to find the placeholder and replace it:

```
<!-- New component -->
<article hidden id="post">
  <h1>Post header</h1>
  <p>Post text</p>
</article>

<!-- Script to replace the placeholder with the new component -->
<script>
  const laterStreamedComponent = document.getElementById('post');
  document.getElementById('post_placeholder').replaceChildren(laterStreamedComponent);
</script>
```

Once all suspended components are sent, the connection with the client can be terminated.

The idea looks good, but what happens when a browser without JavaScript support requests such a page? The infinite spinner or a placeholder skeleton will be displayed for the suspended components. This is a reality for the latest Next.JS version at the time of writing (v14.0.3). Here is the [Suspense demo](https://react-drawbacks.vercel.app/) deployed to Vercel ([source code)](https://github.com/ArturAmpilogov/react-drawbacks/tree/main/nextjs-v14/src/app/suspense).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sk6bz3v9iyhy9er0ox8x.png)

This is the default behavior of the current Next.JS implementation, where all components are server components by [default](https://nextjs.org/docs/app/building-your-application/rendering/server-components#using-server-components-in-nextjs).

It will be better to care about devices with a lack of JavaScript support. For example, during almost every big presentation, Rich Harris, the creator of Svelte, [shows](https://youtu.be/0bog8-Ay7CU?t=12759) how Svelte will work without JavaScript.

Suspense also cannot improve slow server responses. The Vercel site, which is built on its main product - Next.JS, often shows bad user experience with slow data fetching.

{% twitter 1734615967630536746 %}



## Web page size

For many years React code by default was bundled into a huge delivery package resulting in a thick web client application. When a user opened any page, the whole JavaScript application was downloaded, and then based on the URL React rendered a specific branch (page). Often these applications took several megabytes to download before rendering could start.

In 2018, the React team added in-box support for code splitting with the [lazy](https://github.com/reactjs/rfcs/blob/gaearon-patch-2/text/0000-lazy.md) function, which developers should manually call. 

This approach was also used in routing frameworks like [react-router-dom](https://reactrouter.com/), created by Remix authors, and allowed an application to be split into smaller bundles based on the visited URL. 
Currently, Remix and Next.JS create per-page bundles automatically at compile time.

You might think that Server Side Rendering should totally eliminate client bundle problems, but that is not the case with React. The architecture with Virtual DOM and unidirectional rendering flow requires React to recalculate the changes from the top changed component down to all its leaves. To render a component, all the related source code should be presented either as a JavaScript function or with the latest addition of [Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components), in a special JSON format. 

It is not possible to reuse any HTML code as the source of truth, which leads to data duplication. First, the rendered HTML page is delivered to a browser, and second, the same duplicated content is delivered as part of the JavaScript block (a function or a JSON). The second step, called hydration, will allow the framework to react to user interaction events and render changes in response.

In the [second demo](https://react-drawbacks.vercel.app/double-data), the Poem component consists purely of static data. There is a client component that wraps and toggles the poem visibility:
```
<ClientComponentWrapper>
    <Poem />
</ClientComponentWrapper>
```

Please notice that the poem appears twice in the delivered content to a browser: HTML and JavaScript. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8clzzx1jypemp9b3kyns.png)

At the same time, Next.JS was smart enough not to include static parent HTML in the JSON script. This React limitation led to the official Next.JS recommendation [To move Client Components Down the Tree](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#moving-client-components-down-the-tree). 

The suggestion does not work once you have the common scenario with the React context client component wrapping all other page elements, for example, with a top-level user sign-in state:
```
<UserSignInContext>  <!-- Top level client component -->
   <Navigation /> <!-- depends on the user sign in status -->
   <MainContent /> <!-- depends on the user sign in status -->
</UserSignInContext>
```

There are better-optimized solutions. 

Miško Hevery, creator of Angular and Qwik, [re-uses HTML data](https://youtu.be/LbMRs7l4czI?t=4021) in Qwik as much as possible during client components rendering. This is achieved by a light JSON storage with references to the existing HTML elements. No data duplication. The same concept is used in the [Fresh](https://deno.com/blog/intro-to-islands) framework based on Preact and Deno.

```
<!--qv q:id=0 -->
    <p>Hello Qwik</p>
<!--/qv-->
```

Also, Qwik automatically divides components into the [smallest possible chunks](https://qwik.builder.io/docs/concepts/progressive/), and no JavaScript client component is executed until a user interacts with some part that requires it.

## Default unoptimized behaviors

Once React receives a change in a top component, it will re-execute all child components, even those with unchanged properties. All local variables of the components will be recreated, including functions. The [Tree Rerender](https://react-drawbacks.vercel.app/tree-rerender) demo shows what happens with the table function components once an unrelated input field is changed ([source code](https://github.com/ArturAmpilogov/react-drawbacks/tree/main/nextjs-v14/src/app/tree-rerender)).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/67sskkdzje8ef0carv5s.png)

The reality is that by default React burns users’ CPUs. 

The proposed solution by React is to manually wrap all related components into the [memo](https://react.dev/reference/react/memo) function, local functions into “useCallback” function-hook, and local state data in “useMemo” function-hook. This procedure is tedious and error-prone.

The alternative to React hooks is the Signals technology, which precisely executes only the changed dependent components and provides automatic memoization. SolidJS, Preact, Qwik, Svelte, and Angular use [Signals](https://vuejs.org/guide/extras/reactivity-in-depth.html). While the React team has [reasons](https://dev.to/this-is-learning/react-vs-signals-10-years-later-3k71) not to switch to it, the problem of the unnecessary CPU load of user devices is still here. The issue might be resolved this year with the [React Forget](https://www.youtube.com/watch?v=lGEMwh32soc&embeds_referring_euri=https%3A%2F%2Fdev.to%2F&feature=emb_imp_woyt) compiler, 11 years later after the React release.

The current situation is that React forces developers to do the next manual optimizations:
- Split code manually by the usage of lazy function and Suspense wrappers.
- Accurately operate with “memo”, “useMemo”, “useCallback” functions.
- Manually specify which component is the client one by adding [use client](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#conventions-for-serverclient-components) directive.
- Use “startTransitiom” and “useDeferredValue” to help React [faster](https://react.dev/reference/react/startTransition) update the UI.

In comparison, Qwik does the best possible [automatic optimization](https://www.youtube.com/watch?v=tJY1zrW35q8) for all previously mentioned cases out of the box. 

There are additional optimizations that reduce JavaScript execution. Qwik eliminates the Hydration step with the [Resumability](https://qwik.builder.io/docs/concepts/resumable/). Svelte makes direct DOM updates and does not use Virtual DOM structure, drastically reducing CPU and Memory usage. Astro does not load JavaScript until it is specified by a developer. 

## Conclusion

Today, React is the most popular JavaScript web framework. At the same time, its recommended usage produces many performance drawbacks on user devices. It will be great to see from React the CPU and memory usage reduction of hundreds of millions, or even billions, user devices worldwide as a priority.