Optimization is one of the most important aspects of every developer, especially when building web apps. With the use of a virtual DOM, React makes UI updates as efficient as they can be.
How React works
Each React application is composed of many components in a tree formation. Components are functions that render the UI based on the props they receive. Whenever there is a change in data, React computer the differences between the current and the new UI and then proceeds to only apply UI changes to the real UI on the browser. The repeated comparing and rendering components can be one of the main causes of React performance issues.
We want React to only re-render the components that are affected by the change in the data they receive (state or props). In this article I will show you 8 different techniques to improve the app's overall performance. Let's get to work!
- Avoid using index as key
- UseEffect() and UseCallback()
- React.Memo
- React.Fragments
- Lazy loading
- Progressive images
- JS Animations Instead of CSS Animations
- Production build
0. Setup
Let's start by creating a basic React functional component that fetches data from an API with axios and displays the list to the UI. Our state keeps track of loading, errors and the dataU coming in. By combining useEffect with useCallback we make sure our API fetch is not called again on each render.
For the API I will pick a random fun public API, the Cocktail Database API. Here you can find a list of free public API's.
Create a new React app npx create-react-app perf-test
and load up the code above.
Measuring performance
We will use the Chrome Performance Tab to measure the performance of our React app, which is what React suggests. Make sure to disable all Chrome extensions, especially React DevTools. As they can significantly skew the results. I'm also throttling my CPU to 6x slowdown to reproduce a bigger amount of data and slower machine.
1. Avoid using index as key
The example I've created above fetches a list of 25 cocktail recipes, and gives the user the possibility to add their own recipe to the list.
The addCocktail() function updates our state hook of cocktails when a user adds a new cocktail. With useRef() we can refer to the input fields and make sure they are not empty.
The problem in this example is that the component re-renders completely everytime we add a new recipe. When you enable paint flashing in Chrome Devtools, you can see which DOM nodes gets updated.
Render time: 336ms
This is because every cocktail in our array has been pushed one index to the right. A great improvement would be to use unique ID's instead of the indexes. You can use the npm package uuid to generate unique ID's.
...
const updatedCocktails = [
{
idDrink: uuidv4(),
strDrink: currentName,
strInstructions: currentDescription
}
].concat(cocktails);
...
cocktails.map((cocktail, index) => {
const { idDrink, strDrink, strInstructions } = cocktail;
return (
<div key={idDrink}>
<strong>{strDrink}</strong> - {strInstructions}
</div>
);
})
...
Render time: 233ms
Awesome! Let's continue.
2. useEffect() and useCallback()
We are using the useEffect() hook to fetch the cocktails as soon as the component has mounted. It will only re-run when the dependency changes (, it this case the getCocktails function. With useCallback() we are making sure not to fetch the API data every time our App component re-renders.
In our example this won't make such a big difference, but when you have a huge component with many children, it can make a big difference to not re-render the component completely when, in this case, getCocktails changes the state or props of the parent component.
function App() {
const getCocktails = useCallback((query) => {
axios
.get(`https://www.thecocktaildb.com/api/json/v1/1/search.php?f=${query}`)
.then((response) => {
setCocktails(response.data.drinks);
setIsLoading(false);
})
.catch((error) => {
setErrors(error);
});
}, []);
useEffect(() => {
getCocktails("a");
}, [getCocktails]);
}
In the code above, the effect will re-run whenever getCocktails
changes to make sure it has the latest version of getCocktails
. The getCocktails
function will be re-created everytime App
re-builds without using the useCallback
function and will call for an infinite loop when it changes the state or props from App
.
useCallback
helps you prevent this by wrapping it around a function declaration and defining the dependencies of the function, it ensures that the function is only re-created if its dependencies changed. Hence the function is not re-built on every render cycle anymore.
3. Memoize React Components
React.Memo is a Higher Order Component (HOC) that wraps around another component by memoizing the result, which means that that React will skip rendering the component, and reuse the last rendered result. This can give your app a performance boost.
We can store our cocktail div in its own stateless functional component and wrap it with React.Memo().
// index.js
...
cocktails.map(({ idDrink, ...otherProps }) => (<Cocktail key={idDrink} {...otherProps} />))
...
// Cocktail.js
import React from "react";
const Cocktail = ({ strDrink, strInstructions }) => {
return (
<div>
<strong>{strDrink}</strong> - {strInstructions}
</div>
);
};
export default React.memo(Cocktail);
Render time: 192ms
4. React.Fragments
It is common to have multiple components within a component in React. You always need to wrap your children into 1 main component. With Fragments, you can avoid adding more DOM nodes for your main wrapper component. You can use the <Fragment>
tag and import it from React, or use empty tags <></>
Example:
return (
<>
<h2>Cocktails</h2>
{!isLoading ? (
cocktails.map(({ idDrink, ...otherProps }) => (
<Cocktail key={idDrink} {...otherProps} />
))
) : (
<p>Loading...</p>
)}
</>
);
In our example the difference is minimal, but if you have hundreds of thousands of components which are using div's it can make a big difference in performance.
5. Lazy loading
Another native method from React is the React.lazy function, which will load the requested component as soon as the current component has renderered.
For example:
// Normal
import Home from '../screens/Home';
// Lazy
const Home = lazy(() => import("../screens/Home"));
The lazy component has to be called within a <Suspense>
component so that the user sees a fallback item while the component is loading.
<Suspense fallback={<Fragment>Loading...</Fragment>}>
<Switch>
<Route exact path="/" component={Home} />
</Switch>
</Suspense>
6. Progressive image loading
Ever seen the images on Medium.com blurry while they were loading? They are using progressive image loading, which basically means you show a lower quality version image while the high res image is loading.
The react-progressive-image package is a great way to integrate this into your app.
...
import ProgressiveImage from "react-progressive-graceful-image";
import ProfileImgLarge from "../assets/img/profile-large.jpg";
import ProfileImgPlaceholder from "../assets/img/profile-placeholder.jpg";
...
<ProgressiveImage
src={ProfileImgLarge}
placeholder={ProfileImgPlaceholder}
>
{(src) => (
<ProfileImage src={src} alt="Profile of Sander de Bruijn" />
)}
</ProgressiveImage>
...
Using this technique you can show your users images directly by using e.g. <10kb images as placeholders.
7. JS Animations instead of CSS animations.
Many developers actually think that CSS animations are more performed than JS animation, but this article shows the opposite when using complex animations. Besides that, JavaScript-based animation delivers far more flexibility, better workflow for complex animations and rich interactivity.
For simple animations, CSS works just fine. But for more complex ones, I would recommend using the GSAP library.
8. Production build
This has the biggest impact of all. On development, React servers tons of add-ons to make our life easier. However, users do not need these add-ons. By executing yarn build
(or npm build) webpack builds the output folder for us when using create-react-app.
Render time: <60ms
That is it! Hope you learned something from this tutorial. Make sure to follow me for more tips and tricks.
Top comments (10)
In an example from 2nd technique, wouldn't it make more sense to just use empty array as useEffect dependency?
This is possible, but the linter will tell you that there is a missing dependency, in this case getCocktails(). useEffect is depending on external values that may change, when you set the dependencies as an empty array the effect will run just once when the component mounts.
Nice post overall and thanks!❤️
Can you please re-explain your last point please? Thanks :)
Thanks! While you are developing your app in your local development server, React includes many helpful warnings. These warnings are very useful in development. However, they make React larger and slower so you should make sure to use the production version when you deploy the app. See: reactjs.org/docs/optimizing-perfor...
Nice article 👍🏾
Nice post 👍
Thanks!
Nice post bro is very useful, I didn't know about the first three techniques, especially useCallback
Is Suspense still experimental?
Thanks, nice articile :)