If you've ever made a web application with React and are worried about how it will perform on different platforms/browsers/networks etc, then you need to look at its performance. It's most likely that while doing that create-react-app
command, the ever-growing JavaScript library to build user interfaces has already done a few optimisations when you ran that command. So, why do we need to check performance again?
Well, to be fair that command has done 50% of your work already. Now think your application is out on production, you made an e-book store which is used by thousands, they're making their accounts, purchases, adding items to cart, reviewing items and more. With so much heavy-lifting, it makes sense to actually enhance our web app by testing it under different conditions like simulating it on a lower-end mobile device with slow network speed. After all user experience matters the most!
Let's see how you can improve a slow or not-so-responsive React app in the following 5 fixes:
1οΈβ£ Code Splitting
Of course, this comes built-in with Webpack, so naturally React has it too. Let's take that e-book app example. When you first visit the website you've downloaded all the JavaScript code used to run it. Even if it has 4-5 pages in navigation, your browser got each and every byte of data for the entire app. What if we limit this to this that when a user needs a specific page, only that code is loaded/fetched which is necessary.
Code splitting is the way which allows you to split your code into various bundles which can then be loaded on-demand.
This approach is used so that we get smaller bundles of data along with prioritizing which component to load where. All of this, if implemented correctly will have a major impact on your entire application.
We can use React.lazy
π΄ along with Suspense
π to make it happen. React lazy
is a component that allows us to render a component's import
dynamically in the form of a regular component.
Here's how we do it πͺ
Suppose we have a HomePage
component which we want to render lazily, we pass it as:
const LazyHomePage = lazy(() => import('.pages/homepage/homepage.component'));
The lazy
takes a function that calls a dynamic import()
inside which we pass in the path to our component. Next, we need to remove the actual import statement we're making up until this point. Now comes Suspense
. This is used to defer rendering part of your application tree until some condition is met. The LazyHomePage component you made above can now be used inside the new <Suspense />
block as:
...
<div>
<Suspense fallback={<CustomLoader />}>
<LazyHomePage />
</Suspense>
</div>
...
The fallback
property you see above can point to any HTML element or some other component. The best use case I see of this is to make some sort of loading indicator page so that when the user navigates from one page to another within the application, this CustomLoader
is shown as a fallback
.
To learn more about Suspense
, check out Eyal Eizenberg's article:
Trim the Fat From Your Bundles Using Webpack Analyzer & React Lazy/Suspense
Eyal Eizenberg γ» γ» 9 min read
itnext.io
2οΈβ£ Error boundaries
What if while fetching the data from a backend server, your users aren't able to process the payments of the book because there seems to be a network issue? In fact, if an error comes back we really need something to handle it because the Suspense
thing you did earlier will not know what to do in that situation.
Error boundaries to the rescue! π¦ΈββοΈ It's simply a way for us to write a unique component that will catch an error and renders some fallback interface instead of just that error coming the way of a customer visiting your online store.
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.
We can easily convert a class component to an error boundary if it defines the getDerivedStateFromError()
or componentDidCatch()
lifecyle methods. In short the getDerivedStateFromError()
is used to render a fallback UI after an error has been thrown and componentDidCatch()
is used simply to log out the error information to the console.
Here's how we do it πͺ
Inside of our component's local state let's have a boolean property called hasError
which lets us know whether the state has an error in it or not. Initially, it's false. Inside our getDerivedStateFromError()
method we do this:
static getDerivedStateFromError(error) {
// Process the error here
return { hasError: true };
}
It takes the error
parameter be as default and we return the object representing the new state which we want to set locally. For componentDidCatch() we can simply use console.log(error);
Now we conditionally want to return a different UI depending upon whether or not the local state's hasError
property is true
or false
. This is how we do:
render() {
if(this.state.hasError) {
return <ErrorComponent />
}
return this.props.children;
}
We can return an ErrorComponent
which may contain a layout similar to this...
Then finally, we can wrap the Suspense
inside of this new ErrorBoundary
component we wrote above.
3οΈβ£ Component level optimizations
This can be classified into:
shouldComponentUpdate
React.memo
React.PureComponent
The shouldComponentUpdate
is a lifecycle method which we have access to in a class component. It receives two nextProps
and nextState
. This comes in handy when we want to save our application from unnecessary re-rendering. Here's a small example:
shouldComponentUpdate(nextProos, nextState) {
console.log('method called', nextProps);
return nextProps.text !== this.props.text;
}
This method is invoked before rendering when new props or state are being received.
React.memo
is similar to the PureComponent
which we will discuss later. The main difference is that it's used for functional components while the later is used for class-based components. In a simple sentence:
It allows the component to check the old props against new ones to see if the value has changed or not.
Here's how we do it πͺ
React.memo
is a Higher-Order Component (HOC) so just like other HOCs you can start memoizing by passing your component inside React.memo()
like so:
export default React.memo(BooklistComponent)
Finally, React.PureComponent
comes with shouldComponentUpdate()
lifecycle method and implements it with a shallow prop and state comparison. You should use React.PureComponent
instead of the usual React.Component
when the render()
function outputs the same result given the same props and state. This certainly gives a performance boost to your React application.
4οΈβ£ Using React Hooks
The two major hooks to solve performance issues are:
useCallback()
useMemo()
The
useCallback
hook allows us to memoize a function that we wrap in it and use that same function if it already exists.
Syntactically, it has two arguments; first is the function we want to memoize, the second is an array of dependencies and it's mandatory for it to work. If your function doesn't depend on anything, you can simply pass an empty array like so:
const loggerFunction = useCallback(() => console.log('This logs via useCallback'), []);
As it's memoized, this callback will only change if one of it's dependencies were changed. This definitely helps to optimise child components that rely on others to prevent re-rendering of components, thus improving load times!
The
useMemo
hook is especially useful when you want to avoid expensive calculations on render. It only recomputes the memoized value when one of the dependencies has changed.
Here's how we do it πͺ
Let' say we have a function which has to do some complex calculations, here how it's before the useMemo
hook:
const aComplexFunction = () => {
console.log("I'm computing a complex problem!");
return ((firstNum * 1000) % 15.6) * 8340 - 5489;
}
This is after the hook:
const aComplexFunction = useMemo(() => {
console.log("I'm computing a complex problem!");
return ((firstNum * 1000) % 15.6) * 8340 - 5489;
}, [FirstNum]);
Just like the useCallback
, this one also takes an array of dependencies. If no array value is provided, a new value will be computed on every render.
5οΈβ£ Using React Profiler
It's a component that allows us to check how much time (or cost) it takes for our component to render or mount.
If you've played with the React Dev Tools extension's Profiler tab, it's similar to that.
Here's how we do it πͺ
We just wrap it around whichever component we want to measure the cost. It takes two properties; the first is an id
(String) which's an identifier to distinguish which Profiler is logging which component as Profiler can be used in multiple components, the second is a callback function called onRender
which can take multiple arguments. Most common ones of those are id
(String identifier we passed in), phase
(either "mount" or "update") and actualDuration
(time in milliseconds to render the component).
<Profiler id="Homepage" onRender={(id, phase, actualDuration) => {
console.log({id, phase, actualDuration});
}}>
<HomePage />
</Profiler>
Read more!
- https://reactjs.org/docs/optimizing-performance.html
- https://medium.com/myheritage-engineering/how-to-greatly-improve-your-react-app-performance-e70f7cbbb5f6
- https://www.codementor.io/blog/react-optimization-5wiwjnf9hj
And there you go! I hope I've explained these five fixes in the right way. This is my first article on React so if you find any mistakes or suggestions feel free to write them in the comments! π
Don't think comments count... π€
β Microsoft Developer UK (@msdevUK) January 13, 2020
Source: https://t.co/A8UgpqYkLi#DevHumour #IDE #Programmer pic.twitter.com/LSJCtRBFyY
Top comments (0)