Check out more articles:
- Building a Scalable Notification System with gRPC and Microservices
- Adding a Notification Feed in React Websites
- A Complete Guide on Notification Infrastructure for Modern Applications in 2023
Applications can generally be categorized into having two types of performance bottlenecks:
- I/O-bound: These applications spend the majority of their time dealing with inputs and outputs.
- CPU-bound: These applications spend the majority of their time engaged in computational tasks.
Now, how do these classifications translate into the context of front-end applications, particularly React apps?
I/O Performance Challenges in React
When it comes to React applications, issues often arise in terms of I/O performance, primarily related to asynchronous HTTP calls. Ineffectively managing these network requests can lead to a slowdown in the application. While this post primarily focuses on CPU performance, it's essential to briefly touch upon key areas where solutions can be found for I/O-bound problems:
- Implement lazy loading whenever possible.
- Exercise caution during the initial loading of assets and backend requests.
- Reduce the frequency of loading highly static elements (e.g., select options, configurations).
- Debounce the number of times specific requests are made.
- Parallelize requests as much as possible using techniques like Promise.all.
- Enhance the efficiency of critical backend endpoints by optimizing database accesses, among other measures.
CPU Performance Challenges in React
The main thrust of this post centers around addressing CPU performance challenges in React. Before delving into the specifics, let's establish a concrete definition of performance:
- Browser applications predominantly operate as single-threaded programs.
- Scripting tasks, such as JavaScript execution, DOM rendering, and event handling, all occur within the same thread.
- A slow JavaScript module can potentially block the main thread.
- If the main thread is blocked, the user interface becomes unresponsive, resulting in a drop in frames per second (fps).
- Responsive UIs aim for a minimum of 30 fps, ideally achieving 60 fps, meaning each frame should compute within 30 ms or less.
In the context of React, this issue becomes critical. When a React component update is triggered, the entire subtree must be rendered in less than 30 ms. This becomes particularly challenging with complex and lengthy component structures, such as tables, trees, and lists, where large-scale re-renders may be necessary.
React Render and Commit Phase
React, at a high level, operates in two distinct phases:
Render Phase:
- Initiated when a component updates, triggered by changes in props or hooks.
- React traverses the component subtree, rendering each child and computing the Virtual DOM (VDOM) subtree.
- Only the "dirty" subtree, affected by updates, needs to be recomputed; the parents of updated components may not require re-rendering.
- The efficiency of this phase is proportional to the size and computational cost of each child component.
- React.memo can be employed to provide hints for a more efficient rendering process.
Commit Phase:
- The render phase produces a new Virtual DOM of the entire UI.
- In the commit phase, React compares the new tree with the previous one (VDOM diffing).
- React calculates the minimum DOM mutations required to reflect the new VDOM tree.
- DOM mutations are applied, updating the UI.
- This phase is inherently efficient by default.
- The entire process must be completed in less than 30 or 16 ms (for 30 fps and 60 fps, respectively) for the UI to be deemed responsive. The workload is directly proportional to the size of the app.
The subsequent exploration will focus on enhancing the efficiency of the Render phase. Before delving into optimization techniques, it is crucial to understand how to measure and identify the sluggish components in the application.
Measuring
Among the tools I frequently rely on are:
- Chrome Dev Tool’s Performance Tab
- React Dev Tool’s Performance Tab
Chrome Dev Tool’s Performance Tab
This tool stands out as a comprehensive resource applicable to any browser application. It provides insights into frames per second, captures stack traces, identifies slow or hot sections of your code, and more. The primary user interface is represented by the flame chart.
For an in-depth understanding of Chrome’s Performance Tab as applied to React, refer to this documentation.
React Dev Tool’s Performance Tab
To leverage this tool, you'll need to install the React Dev Tool extension in your browser. It tailors information from the Chrome Dev Tool’s Performance Tab specifically to React. Through a flame chart, you can observe different commit phases and the JavaScript code executed during the respective render phase.
This tool aids in easily determining:
- When a component undergoes re-rendering.
- What props have changed.
- What hooks have changed, encompassing state, context, and more. For further details, refer to the introductory post.
Measuring Methodology
Here’s the methodology I prefer when assessing front-end applications:
-
Identify the Problem:
- Pinpoint page interactions causing UI responsiveness issues.
-
Create a Hypothesis:
- Optionally, generate ideas about the potential location of the problem.
-
Measure:
- Verify the problem by measuring essential metrics such as frames per second (fps).
-
Measure (Part II):
- Identify problematic sections of code; optionally, validate your hypothesis.
-
Create a Solution:
- Implement a solution based on the insights gathered.
-
Measure the Solution:
- Validate that the implemented solution resolves or alleviates the problem by examining key metrics.
Optimizing without proper measurement renders efforts practically ineffective. While some problems may be apparent, the majority necessitate thorough measurement, forming the cornerstone of the performance enhancement process.
Moreover, measurements empower you to communicate achievements upwards, informing users, stakeholders, and your leadership about performance improvements achieved within specific areas of your application, expressed as a percentage gain.
General Solutions to CPU-Bound Problems in React Applications
Now armed with measurements and an understanding of problematic areas, let’s delve into potential solutions. Optimizing React performance revolves around improving both what components render and which components render.
Many performance issues also stem from anti-patterns. Eliminating these anti-patterns, such as avoiding inline functional definitions in the render method, contributes to more efficient rendering times. Addressing poor patterns can, in fact, reduce complexity and improve performance simultaneously.
🤔 Improving What Components Render
Identifying sluggish components in our React app typically points to specific components that struggle with rendering or have an excessive number of instances on a single page. Various reasons may contribute to their sluggishness:
- Blocking calculations within components.
- Rendering large component trees.
- Utilizing expensive or inefficient libraries.
Most of these issues boil down to enhancing the speed of component rendering. At times, crucial components cannot rely on overly complex libraries, necessitating a return to basic principles and the implementation of simpler alternatives.
For instance, I encountered such challenges while using Formik excessively within multiple cells of every row in a complex table. While improving the efficiency of individual components goes a long way, attention must eventually shift to which components are rendering.
🧙 Improving Which Components Render
This aspect offers two broad categories for improvement:
-
Virtualization:
- Only render components that are visible in the viewport. For example, rendering only the table rows or list items that the user can see. This approach proves beneficial for complex UIs, and while it can be applied without addressing the "what" step, it is recommended. Modern libraries often provide robust support for virtualizing tables and lists, with examples like
react-virtualized
. Virtualization reduces the number of components React needs to render in a given frame.
- Only render components that are visible in the viewport. For example, rendering only the table rows or list items that the user can see. This approach proves beneficial for complex UIs, and while it can be applied without addressing the "what" step, it is recommended. Modern libraries often provide robust support for virtualizing tables and lists, with examples like
-
Props Optimization:
- React aims to make components resemble pure functions but may attempt to render more times than necessary.
React.memo:
-
Most components in React can be memoized, ensuring that with the same props, the component returns the same tree (although hooks, state, and context are still respected). Leveraging
React.memo
informs React to skip re-rendering these memoized components if their props remain unchanged.import React from 'react'; const MyComponent = React.memo((props) => { // Component logic here }); export default MyComponent;
Fake Prop Changes: useCallback:
-
Addressing the issue of fake prop changes involves instances where the content of a prop remains unchanged, but the reference changes. A classic example is an event handler.
import React, { useCallback } from 'react'; const MyComponent = () => {
const onChange = useCallback((e) => console.log(e), []);
return <input onChange={onChange} />;
};
export default MyComponent;
```
Fake Prop Changes: useMemo:
-
Similar challenges arise when constructing complex data structures without proper memoization before passing them as props. Utilizing
useMemo
ensures that rows are recalculated only when dependencies change, enhancing efficiency.import React, { useMemo } from 'react'; const MyComponent = ({ data, deps }) => { const rows = useMemo(() => data.filter(bySearchCriteria).sort(bySortOrder), [deps]); return <Table data={rows} />; }; export default MyComponent;
While you have the flexibility to customize how React.memo
compares current vs. previous props, it's crucial to maintain a swift calculation since it's an integral part of the Render phase. Avoid overly complex deep comparisons during each render.
How does it look now?
Props changed
How it looks in the React dev tool:
Did they really? Are they fake prop changes? Use useCallback
and useMemo
.
Parent rendered
How it looks in the React dev tool:
Use React.memo
to memoize your pure components.
Hooks changed (state, context)
How it looks in the React dev tool:
Nothing too obvious to do here. Try to validate that the hook that changed makes sense. Perhaps a bad context provider is faking out changes the same way as fake prop changes might appear.
Similar to this, I personally run a developer-led community on Slack. Where we discuss these kinds of implementations, integrations, some truth bombs, weird chats, virtual meets, and everything that will help a developer remain sane ;) Afterall, too much knowledge can be dangerous too.
I'm inviting you to join our free community, take part in discussions, and share your freaking experience & expertise. You can fill out this form, and a Slack invite will ring your email in a few days. We have amazing folks from some of the great companies (Atlassian, Scaler, Cisco, IBM and more), and you wouldn't wanna miss interacting with them. Invite Form
You may want to check out a seamless way of integrating your notification infrastructure.
suprsend / suprsend-go
SuprSend SDK for go
suprsend-go
SuprSend Go SDK
Installation
go get github.com/suprsend/suprsend-go
Usage
Initialize the SuprSend SDK
import (
"log"
suprsend "github.com/suprsend/suprsend-go"
)
func main() {
opts := []suprsend.ClientOption{
// suprsend.WithDebug(true),
}
suprClient, err := suprsend.NewClient("__api_key__", "__api_secret__", opts...)
if err != nil {
log.Println(err)
}
}
Trigger Workflow
package main
import (
"log"
suprsend "github.com/suprsend/suprsend-go"
)
func main() {
// Instantiate Client
suprClient, err := suprsend.NewClient("__api_key__", "__api_secret__")
if err != nil {
log.Println(err)
return
}
// Create WorkflowTriggerRequest body
wfReqBody := map[string]interface{}{
"workflow": "workflow-slug",
"recipients": []map[string]interface{}{
{
"distinct_id": "0f988f74-6982-41c5-8752-facb6911fb08",
// if $channels is present, communication will be tried on mentioned channels only (for this request).
// "$channels": []string{"email"},
…
Top comments (50)
I suggest Code Splitting:
Use code splitting to divide your application into smaller chunks, and load only the necessary parts when they are needed. This can be achieved using tools like Webpack.
Although with React you will never get rid of the initial bundle size upon page load, and you'll suffer that penalty each time loading the HTML page as React itself is unsplittable. With Node.js it gets even worse since then you also have it's code included as well so that you get client side routing and all the related issues that go alongside with that.
Unless React dramatically changes how it's been designed there is no way for it to get rid of that code. They must do things like abandon their own event system.
It isn't entirely about the speed of the connection, although that take is still a bit ignorant considering web speed is not always perfect depending on conditions and location.
The parsed JS is often the heaviest bytes on a website. So each time a HTML page boots that code will execute. Not a problem on your high end Apple Mac M(insert latest number here), but vast majority of devices are less performant. And at least I think most sites should work snappily regardless of the device capabilities.
Basically every take where you say "not a problem for me" is you not being aware that what is true for you does not apply to everyone else. Bundle size is both performance and accessibility concern. The bigger your bundle becomes and the heavier it becomes to execute, the more people are effected negatively.
Also in March Google starts to rank sites low if they score low on INP. That metric is awful on about every React site in existance, and that metric is related to initial JS bootstrap time.
So: bundle size does matter, a lot.
The power of microprocessors grow year after year, and yet you see constant performance enhancements in many projects, including .Net (to mention a very large one). What I'm trying to say is: People that say "we won't care about our memory consumption because RAM is cheap", or "bundle size doesn't matter becase Internet speeds are faster year after year", are people unwilling or unable to do the right thing.
Apologies if it feels like it goes your way. It is my honest opinion.
Do you know the size of the core React library bundle? It is less than 3 kB minified an gzipped. It is less than 100 ms of loading size even on 3G. Do you really think it is a bottleneck?
Interesting. Can you point me to a React project in GitHub or elsewhere where once compile will produce a less than 42kb bundle? Because this is the size it is always seen. If you have an example of a React project than bundles around 3kb, I'd love to see it because that's the SolidJS and Svelte bundle sizes, never React sizes.
bundlephobia.com/package/react@18.2.0
Svelte is bigger bundlephobia.com/package/svelte@4.2.8
Thanks. It is an interesting metric, but by all means an incorrect one. The cost of React is 40kb as a minimum. I wonder why bundlephobia reports such a low number? Anyway, thanks for the link. Interesting thing, this bundlephobia. However, it is completely off base when it comes to React at the very least.
Because it is true. Why do you think overthise?
Because I have done the exercise many times:
npm create vite@latest
. SelectReact + Typescript + SWC
.npm i
.npm run build
.I just did it. Here's the screenshot: Out of the box, the sample Vite project is over 143KB, 46kb gzipped.
Would you like to see a Svelte test?
Just FYI, about every React site requires React DOM:
bundlephobia.com/package/react-dom...
So it's 10 + 130 = 140 kilobytes just to parse React code.
Of course on a NextJS project you also need to add client router etc so it can work like a SPA, to make Link components work. That code needs to technically imitate actual page transition to ensure focus is in the right place, screen reader announces the right things and all the other stuff that is provided by native HTML page loading for no extra code.
And we got there because React DOM is so big that it does slow down app bootstrap time.
@webjose Look this:
Finding Svelte's Inflection Point / Calculating the Inflection Point
That was nice, @monoprosito. Definitely quite informative. One more star for Svelte, I suppose. 👌
or Qwik ;)
Nice write-up!
I think the key thing is understanding what you are asking of a framework, if you render thousands of components then it's going to be slow, it's just easy to not think of that when working with the basic principles in React and not accounting for how things will scale.
Your car is slow? May it help to put more fuel in? Likely not.
Just buy a new one...
First, define slow
Ok, wait a minute. I´ll tell you when this damn web site has been rendered....
React isn't as slow
No, but obviously there are a number of stumbling blocks that should be avoided. If it turns out you're spending more time finding the stumbling blocks, then maybe it's time to find a new platform.
AngularJS guys told 10 years ago that below 100ms of latency your users won't notice. Even in a complex app you have to do really crappy job to cross that limit.
If you look at complex & fucked up apps (look at jira for instance), latency is implied by a shitload of unnecessary XHR (graphql is our friend) not so much by the front-end library.
React is not slow enough to lose popularity.
Starbucks and McDonald's were able to expand globally because they were not tasteless...
React is a great piece of software in terms of engineering but it is a terrible choice for the vast majority of projects because it was never meant to be used in the kind of projects most people work on. React's biggest problem isn't React itself; it's the fact that too many people pick their tools because they are shiny, and not because they solve the specific problem they are working on. I wrote about this here (or here, if you want the original).
You can switch to Preact, a fast 3kB alternative to React with the same modern API.
Never heard, any resource?
The resource was on the link ;) Once again: preactjs.com/
Interesting reading. Thanks for sharing!
I wonder how would Promise.all work in practice. Won’t it take longer to wait for all promises?
It is faster in the case where you'll be doing all the requests anyway. It beats the waterfall case of request + wait + complete + request next + wait + complete + request another + wait + complete.
But in the overall picture it isn't a silver bullet, and you probably want to consider techniques that make things appear to be faster instead of being actually faster.
I see. Makes sense
~ 🤡
I have solved all the problems mentioned in this article in my web development framework. We use it endlessly at uiedbook.
github.com/Uiedbook/cradova
i don't but i just saw a post somewhere, they say next js makes it fast or something
Nice Article 👍
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more