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.
The project immediately gained popularity, simplifying web application development with the next core ideas:
- UI components are written in the JSX syntax, 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 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 among JavaScript web libraries. It is the most discussed web framework in StackOverflow and is used in 3.5% of all websites.
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 algorithm, reducing JavaScript file sizes, code splitting, and implementation of islands using component streaming and selective hydration.
Moreover, the recent optimizations were done with high competition pressure from other projects (Angular, 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 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 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 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 to the official documentation to use one of the frameworks with React Server Side Rendering support, like Next.JS or Remix.
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 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 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 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 deployed to Vercel (source code).
This is the default behavior of the current Next.JS implementation, where all components are server components by default.
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 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.
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 function, which developers should manually call.
This approach was also used in routing frameworks like react-router-dom, 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, 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, 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.
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.
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 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 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, 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 demo shows what happens with the table function components once an unrelated input field is changed (source code).
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 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. While the React team has reasons 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 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 directive.
- Use “startTransitiom” and “useDeferredValue” to help React faster update the UI.
In comparison, Qwik does the best possible automatic optimization 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. 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.
Top comments (5)
React is 11
20years old and it shows.There are now much more specialized web frameworks that do specific functions much more better than general rendering library like React itself.
The main thing a web dev has to decide is that he wants/needs:
Best Frameworks / Libraries
Note on Performance
What? React was founded in 2013 so 11 years ago.
You are right
The main reason React got so popular is because of DX (developer experience).
React was the first Framework that did very complex web applications (Facebook).
React biggest strength is HMR (Hot module reload).
If you have complex state and have to reload the page and then redo the 10-20 clicks you need test the UI state -> this becomes quickly unbearable if you doing it 10-100 times a day working on UI without HMR.
Back then, 20 years ago, then React started there was only Angular.js as an alternative. Angular burned much dev community goodwill by breaking its custom DSL each major upgrade. React is still backward compatible and you can use old class components even today.
React, being only a library, with only a single minded goal at doing rendering -> allowed to grow a very big ecosystem of tools/libraries to build upon.
My best/favorite example is react-email / jsx-email -> writing cross platform emails is very painful, the React/JSX model allows to easily use the rendering model of react in other/different domains.
If you want to beat React then you have to provide good tooling and a great developer experience. This is very hard to do. For example, the only reason I'm not switching from React to Svelte (and I tried) is because of missing IDE integration in Idea/Intellij.
This article was a breeze of fresh air.
I often felt with the React-Redux stack that I was not "smart enough" to deal with it, that I make mistakes still, and that I have to really concentrate on doing the right thing. Now both of these feel like they were designed by academic minds, but not crafty, seasoned developers that know how to make obvious designs.
Which is, in reality, probably a design mistake. Have you heard of the "Was it really a human error that crashed the plane?". Things that depend on the freshest brain of a well-rested person are in fact not designed well.
So with Signals, it seems that many of these problems could have been eliminated earlier and at the root.