In today's fast-paced web landscape, a React application's performance is paramount. A sluggish app can lead to frustrated users and lost conversions. Fortunately, React provides a plethora of built-in features and libraries to streamline your application and deliver a seamless experience. Here are 7 optimization techniques that will elevate your React app to production-grade quality
01. Component Memoization
To achieve memoization we can employee React.memo which is a higher-order component (HOC) that prevents unnecessary re-renders of functional components by memoizing the component output based on its props. It re-renders only if the props or the component's state change.
import React from 'react';
const MyComponent = React.memo(({ value }) => {
console.log('Rendered');
return <div>{value}</div>;
});
export default MyComponent;
Using useCallback and useMemo for Stable Reference
Similar to useMemo but for functions, useCallback prevents unnecessary function recreation when its dependencies haven't changed. This is particularly helpful for callback functions passed as props.
import React, { useCallback, useMemo, useState } from 'react';
const MyComponent = ({ items }) => {
const [count, setCount] = useState(0);
const calculateTotal = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0);
}, [items]);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>Total: {calculateTotal}</p>
<button onClick={increment}>Increment: {count}</button>
</div>
);
};
export default MyComponent;
02. Code Splitting Technique
Lazy loading allows you to load components or modules only when they're needed. This reduces the initial bundle size and improves load times, especially for complex applications. React's built-in React.lazy and Suspense components facilitate this process.
import React, { lazy, Suspense } from 'react';
const MyLazyComponent = lazy(() => import('./MyLazyComponent'));
function MyComponent() {
return (
<div>
<button onClick={() => import('./MyLazyComponent')}>Load Lazy Component</button>
<Suspense fallback={<div>Loading...</div>}>
<MyLazyComponent />
</Suspense>
</div>
);
}
03. Efficient Event Handling
For frequently triggered events, consider Throttling or debouncing techniques to reduce the number of function calls. Throttling ensures the function executes at most once within a specified time interval, while debouncing only executes it after a period of inactivity.
Debouncing
import React, { useState } from 'react';
import { debounce } from 'lodash';
const Search = () => {
const [query, setQuery] = useState('');
const handleSearch = debounce((event) => {
setQuery(event.target.value);
// Perform search operation
}, 300);
return <input type="text" onChange={handleSearch} />;
};
export default Search;
Throttling
import React, { useEffect, useRef } from 'react';
const AnimationComponent = () => {
const ref = useRef();
useEffect(() => {
const animate = () => {
// Update animation frame
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, []);
return <div ref={ref}>Animating...</div>;
};
export default AnimationComponent;
04. Rendering Long Lists (Virtualized Lists)
When dealing with extensive lists, list virtualization becomes crucial. It renders only the visible items on the screen, significantly improving performance and reducing DOM manipulation. Popular libraries like react-window and react-virtualized offer efficient solutions
import React from 'react';
import { FixedSizeList as VirtualizedList } from 'react-window';
const ListItem = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const MyList = ({ items }) => (
<VirtualizedList
height={150}
itemCount={items.length}
itemSize={35}
width={300}
>
{ListItem}
</VirtualizedList>
);
export default MyList;
05. Track Performance Bottlenecks
Profiling is the cornerstone of optimization. Use React DevTools Profiler to identify performance bottlenecks in your components. It pinpoints areas consuming excessive rendering time, guiding your optimization efforts.
import React, { Profiler } from 'react';
const 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
) => {
console.log('Render time:', actualDuration);
};
const App = () => (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
export default App;
06. Optimize Context Usage
Using useContextSelector from the use-context-selector library can help in avoiding unnecessary re-renders when using context.
import React from 'react';
import { useContextSelector } from 'use-context-selector';
const CountContext = React.createContext();
const Display = () => {
const count = useContextSelector(CountContext, state => state.count);
return <div>{count}</div>;
};
const App = () => {
const [count, setCount] = React.useState(0);
const contextValue = { count, setCount };
return (
<CountContext.Provider value={contextValue}>
<Display />
<button onClick={() => setCount(count + 1)}>Increment</button>
</CountContext.Provider>
);
};
export default App;
07. Usage of Fragment Components
Avoid unnecessary DOM nodes by wrapping JSX elements with React.Fragment. This prevents creating extra
elements for layout purposes.import React, { Fragment } from 'react';
function MyComponent() {
return (
<Fragment>
<p>Item 1</p>
<p>Item 2</p>
</Fragment>
);
}
// OR
function MyComponent() {
return (
<>
<p>Item 1</p>
<p>Item 2</p>
</>
);
}
Top comments (9)
Profiler
I didn't know that one, it's very instructive π
Memoization
I would be very careful before recommending memoization. While it's more performant in rare cases, 99.9% of the time it's premature optimization.
It's like going to the carnival and you get some tickets for the rides.
You only rarely see case #1, so memoizing should not be your default reflex.
Note that the
useCallback
example serves no purpose whatsoever. If your component re-render, it will re-render the button too, nothing will be lost by not using it. This is the perfect example of premature optimization. The code is less readable without any gain to show for it.Optimize context usage
This is greatly related to premature optimization from the previous point.
If you need that kind of optimization, you might have a problem in how the context architected. Sure you get re-renders with the normal context, but if it's a problem then there's something else that you can do to prevent it.
Loading 2 extra libraries for the rare case you'll need it is not an optimization. At this point, if you really need something like this, might as well go for
zustand
that will give you so much more than this and will be mostly tree-shakeable. You might even load less JS in the end with Zustand, not that you should need it in most cases.Efficient event handling
Again, I would be careful speaking about optimization if your examples are not optimized.
The
debounce
example is loading the wholelodash
library...lodash
is not tree-shaking like it's supposed to and this kind of import is bringing a whole lot of JS for nothing. Check on thelodash
npmjs.com page how to adjust the importing correctly.The
throttle
example is not a throttle... It's the equivalent of asetInterval
, which is not the same process as throttling. If you were to mount/unmount that component repeatedly in a few milliseconds interval, you'd get an inordinate amount of calls torequestAnimationFrame
. Throttling would not allow this.I will learn more about the 'Fragment' component. It is useful when we want to return multiple elements from a components render method without wrapping them in an unnecessary div. However, I mostly use container components for grouping. Thanks.
Concise and effective. Great reading. Thanks!
Great article. I learnt about new tools.
Thanks for sharing
Nice read. I didn't quite understand why we need useContextSelector. Shouldn't the components that use the context re-render. Isn't that the advantage?
Context causes a re-render for every update, even if the update doesn't cause the component to change. Imagine storing the data from a Contact form in Context:
If you update the
name
, theemail
component doesn't need to re-render... nothing relevant changed foremail
. If you use context directly, you will re-renderemail
whenname
changes.useContextSelector
will run a smaller function to determine if the context update caused a relevant change, and avoids re-rendering components when the data they use hasn't changed.Or just use new react compiler, even though it is in beta)))
I might add
the instruction for the appropriate use of useEffect with the appropriate dependencies, very often underestimated
Why useMemo for components and not useCallback?
Some comments may only be visible to logged-in visitors. Sign in to view all comments.