DEV Community

loading...
Cover image for 5 quick and easy fixes to improve your React app performance πŸš€

5 quick and easy fixes to improve your React app performance πŸš€

vaibhavkhulbe profile image Vaibhav Khulbe ・7 min read

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.

404 error

Just so that your users don't do this γƒΎ(≧ β–½ ≦)ゝ

To learn more about Suspense, check out Eyal Eizenberg's article:

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...

404 error GIF

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:

  1. shouldComponentUpdate
  2. React.memo
  3. 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:

  1. useCallback()
  2. 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!


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! 😁


πŸ“« Subscribe to my weekly developer newsletter πŸ“«

PS: From this year, I've decided to write here on DEV Community. Previously, I wrote on Medium. If anyone wants to take a look at my articles, here's my Medium profile.

Discussion

pic
Editor guide