React performance - how to
Marko Rajević
Posted on March 30, 2022
In this post, we will go through some techniques and solutions to achieve good performances in your React application.
Dynamic import
Your app doesn't need to be one big bundle because you don't need all parts of your application immediately.
If you build a website with multi-pages you need the current page to be loaded immediately and other later when the user request those.
import { useState } from 'react';
import dynamic from 'next/dynamic';
const Modal = dynamic(() => import('../components/Modal'));
function Home() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(!showModal)}>Toggle modal</button>
{showModal && <Modal />}
</div>
)
}
export default Home
Next.js does this by default for you. It will create separate smaller bundles for each of your pages (routes).
Furthermore, you can dynamically load components and parts of the application which are not visible by default like modals or panels.
In the example above code for Modal
will not be loaded until the component is rendered which means that your main bundle will be smaller and the initial page load faster.
If you are not using Next.js the same thing you can achieve with React.lazy.
React.memo
One thing that you don't want from your React app is unnecessary rerender 🙂.
If you wrap your component with React.memo
you can ensure that your component will rerender only on props or state change, not whenever the component parent rerender.
React.memo
compares prev and next props and if those are the same React will skip rendering the component, and reuse the last rendered result.
By default, props are compared shallowly but you can provide your custom comparison function as the second argument.
function MyComponent(props) {
...
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
When to use React.memo
is up to you, my recommendation is to use it when you have a problem with the performance and rerenders of your component is too expensive.
Also, you can use it by default for the components with a lot of elements, like the lists or the tables.
How to properly use useCallback
with React.memo
you can check in my previous post here.
Profiler
Measure performances.
A great way to locate the components which are rerendering too many times or render slowly is to use Profiler
HOC.
More about it you can read it here.
For the component you want to measure performances you need to wrap it with Profiler
component.
Props that you need to pass to the Profiler
are id
and onRender
.
return (
<App>
<Profiler id="Navigation" onRender={callback}>
<Navigation {...props} />
</Profiler>
<Main {...props} />
</App>
);
Also, you can have multiple Profile
components at the same time and you can nest them to measure the performances of different components within the same subtree.
onRender
callback provides the next, very useful, informations.
function onRenderCallback(
id, // the "id" prop of the Profiler tree that has just committed
phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime, // when React began rendering this update
commitTime, // when React committed this update
interactions // the Set of interactions belonging to this update
) {
...
}
The most important information here is actualDuration
, which shows how much time the component is needed for that current render.
Compare this time with baseDuration
which is the time needed to render the component and entire subtree without memoization.
useMemo
This hook can help you if you create an object or an array within your component and that creation is time expensive.
It accepts two parameters. The first one is the function that returns the value you want to memoize and the second one is an array of dependencies.
If any of the dependencies change useMemo
will recalculate the value, otherwise will return memoized value.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
When to use it?
Well, I think that you can recognize operations that are expensive and can be memoized.
For example, if you have the map
function within another map
function and you are working with long arrays, that will be slow and it's good to be memoized.
Off course, you can measure how much time is needed for a specific operation and decide based on that.
For this purpose performance.now() can be used.
react-window
React
is not very performant when it comes to rendering large lists or grids.
To resolve this problem plugins like react-window can be used.
The strategy is to only render the number of items that are in the viewport.
From the documentation:
- It reduces the amount of work (and time) required to render the initial view and to process updates.
- It reduces the memory footprint by avoiding the over-allocation of DOM nodes.
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Example = () => (
<List
height={150}
itemCount={1000}
itemSize={35}
width={300}
>
{Row}
</List>
);
Good practices
Debounce function.
It's not directly related to React
but it can be applied.
If you call a function on an event that often occurs it's a good practice to debounce it.
You can use the debounce
function from some library like Lodash or create your own.
function debounce(func, timeout = 250){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
Now, for example, if you need to do something on window resize
or scroll
, it can be written like this:
useEffect(() => {
const onResize = debounce(function() {
// The function's code
}, 250);
const onScroll = debounce(function() {
// The function's code
}, 250);
window.addEventListener('resize', onResize);
window.addEventListener('scroll', onScroll);
return () => {
window.removeEventListener('resize', onResize);
window.removeEventListener('scroll', onScroll);
}
});
Think how you organize your components.
For example, if you have this component:
const ItemsList = ({ items }) => {
const [inputValue, setInputValue] = useState('');
return (
<div>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
{items.map((item) => {
...
})}
</div>
)
}
The problem with this is that the entire component will rerender on every input change which is not optimal because besides the input there is the list of items as well which stays unchanged.
A better approach would be to move input
out of the component and wrap the ItemsList
component with React.memo
so it can depend only on the items
prop.
const ItemsList = React.memo(({ items }) => {
return (
<div>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
{items.map((item) => {
...
})}
</div>
)
})
const ParentComponent = () => {
const [inputValue, setInputValue] = useState('');
const [items, setItems] = useState([...]);
return (
<div>
<input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<ItemsList items={items} />
</div>
)
}
That's all, have fun and create performant React
apps. 😉
Posted on March 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 22, 2024