3 Optimizations to Minimize Rendering in React

Render Delays

Rendering is the most pleasing part of the React component lifecycle. It is the moment when you, as a developer or user, see the results of your work designing components, manipulating initial state, and grabbing data from your backend. But is also the part where any UI or UX deficiencies become obvious. Unnecessary renders can really hamper the user experience of your application, and with large-scale apps that require a lot of time, effort, and research to put together, it is tempting to overlook rendering enhancements during the push for a viable product.

Fortunately, refactoring to minimize rendering is usually easy if you were wise enough to use a store to manage state). Modern JavaScript, CSS optimization, and small components provide us with a vast set of tools to minimize our renders, improve the user experience, and actually make our code more readable. Here are three tricks I learned while building and refactoring the Succotash app.

Optimization #1: Batch State Updates when Making Multiple Asynchronous Requests to your APIs

I generally prefer to minimize backend fetches on the initial load of my applications that require user login by nesting data extremely deeply. Sometimes, however, you may have to fetch user data from your API and informational data from an external API, or, as was my case with Succotash, grab both data specific to the user and another set of data that was crowdsourced across the app's userbase from different backend routes.

In a situation where we have multiple fetch requests that need to be made on the mounting our application, we have four options:

  1. We can make two separate fetch request orders synchronously in our code. This situation, however, will mean we are unable to take advantage of React's asynchronous updates of state. But after which async request should we attach our request to end our loading state? For an app like Succotash, where only one call requires our database to decode a JSON web token while the other call has a more accessible route, we can fairly safely assume that the user-related fetch will take longer to process than the non-secure list grab. But if your app is making both a secure user-login request to your own API and a request to an outside API that may have its own verification time, which could also take longer or shorter depending on volume, the choice is not so clear cut.
    In pseudo-code, this would look like:
        
    componentDidMount() {
        displayPageLoader()
        
        fetch(userDataUrlWithHeader)
        .then(userData => userData.json())
        .then(user => {
            putUserInState(user)
            endPageLoader()
        })
    
        fetch(crowdsourcedDataUrl)
        .then(crowdsourcedData => crowdsourcedData.json())
        .then(putCrowdsourcedDatainState)
    }
        
    
    When I tried this type of request through the development version of Succotash on localhost servers, the full page load took 3.49 seconds, and my render hook was hit five times (preloading, loading state, deliver crowdsourced data, deliver user data, end loading state).
  2. We can chain our asynchronous fetch requests, so that the second only starts once the first one ends. That will also allow us to securely end our loader at the end of the chain. Again with pseudo-code:
        
    componentDidMount() {
        displayPageLoader()
        
        fetch(userDataUrlWithHeader)
        .then(userData => userData.json())
        .then(user => {
            putUserInState(user)
            return Promise.resolve(true)
        })
        .then(() => {
          fetch(crowdsourcedDataUrl)
          .then(crowdsourcedData => crowdsourcedData.json())
          .then(data => {
            putCrowdsourcedDatainState(data)
            endPageLoader()
          })
        })
    }
        
    

    With this set-up, our page generally loaded anywhere between 3.1 and 4.5 seconds on a localhost, but we witnessed 5 renders: (preloading, loading, with the user but no crowdsouced data, end with both the user and crowdsourced data, and finally without the loaded).
  3. JavaScript's Promise.all() sound like a likely savior here, especially if you are not using a state store. (Succotash does.) You could then pass down the batched results to child components together, splitting them up in a lower component when necessary. In pseudo-code, our componentDidMount would look like:
        
    componentDidMount() {
        displayPageLoader()
    
        Promise.all([
            fetch(userDataUrlWithHeader),
            fetch(crowdsourcedDataUrl)
        ])
        .then(responses => {
            responses[0].json()
            .then(putUserInState)
            
            responses[1].json()
            .then(putCrowdsourcedDatainState)
    
            return Promise.resolve()
        })
        .then(() => endPageLoader())
    }
        
    

    This option is the quickest yet, but comes with its own host of problems. Most of my tests showed a page load of under 3 seconds, albeit with 5 renders. But take a look at the code. The promise is returned before the asynchrounous updates conclude, which means the loader disappears before the fetched data is rendered. Of the user data and crowdsourced data, sometimes the user data would render first, while in other instances it was the crowdsourced data. This unpredictability is not good. In all cases, the loader stopped before either the crowdsourced or user data came through. Oftentimes, the console would also display a violation warning: 'Forced reflow while executing JavaScript took 41ms.' While quick, the loader disappearing before the server concludes its tasks is a no-go and would confuse the user if there was a problem with the server.
  4. Finally, in my preferred solution, if we use Redux and async/await, we can use Promise.all() to batch our data into one succinct JavaScript object and pass that through as a unique action for our reducers to handle. Our new setLoggedInState reducer can set our user and crowdsourced data into state and also take the loader off our page.
        
            // App.js
    
    componentDidMount() {
        displayPageLoader()
        let action = {}
    
        Promise.all([
            fetch(userDataUrlWithHeader),
            fetch(crowdsourcedDataUrl)
        ])
        .then(async (responses) => {
            
            await responses[0].json()
            .then(user => {
              action['user'] = user
            })
      
            await responses[1].json()
            .then(crowdsourcedData => {
              action['crowdsourced'] = crowdsourcedData
            })
            return action
          })
          .then(data => {
            setLoggedInState(data)
          })
    }
    
            // actions.js
    
    export function setLoggedInState(initialState) {
        return {type: 'INITIALIZE_APP', ...initialState}
    }
        
    

    This cuts our rendering count down to only three (pre-loading, loading, and everything updated in state). Like our other Promise.all() option, the load time varies greatly, with some really long load times, but we average less than the both the synchronous and chained fetch options. We also see the loader disappear at the same time our data appears -- not before.

Optimization #2: Only Map State To Props When Absolutely Necessary

Using state store management allows us to have entire components that need not view state. Static pages, like Succotash's how-to page, are perfect examples of fairly large scale components that do not need to read from the store.

The images on that page are another matter. For the feature that magnifies images when you click them, I needed to dispatch an action to the reducer that mounted the Material-UI dialog box that held the larger image. Material's React dialog boxes take in a prop 'open', that, when true, mounts to the page. What I found when I passed my prop from state to the component (and also when I closed the dialog box) was that all the images on the page would go through a reload because of the updated prop received from the store.

The solution was to move the dialog box component down a level, so that the component holding the clickable image could take in no props from state. A new component was then nested inside the how-to page's image component, only passing the image URL as a prop. That inner component then held the dialog box, which was mapped directly to state.

While in many cases, such a re-render would be barely noticeable, images are extremely obvious when they need to re-render, so this trick can help manage components, as well as abbreviate some of your components' codebase.

Optimization #3: Use Different Progress Loaders for Different Components

While it would be great for only a few select components to need state mapped to their props, in most apps, this is just not the case. And when props update with a connection to the backend, we need to use loaders to avoid some really buggy user experiences.

The lazy solution is to put one reducer on the very top of the app, and load it at each fetch and unmount it once our promises resolve and our state updates. You could, of course, use tip #2 here to avoid extra renders. (Place a full-page loader in its own mini-component, but set it with full-page and z-index of -1 styling attributes.)

For a slow-processing backend, however, this might be a little problematic from a UX perspective. Blocking an entire app for the sake of one component or unit is not an ideal set-up. The solution is to place a loader over just the component, rather than the entire page.

In Succotash, this solution is implemented in the deadline list component, visible on both the profile view and the field view. Replacing the entire component with a circular loader would result in a very jerky motion of the visible component when our backend is operating at ideal speeds, so that is an awful solution. Instead, I used a wrapper div with relative positioning for the component's root returned node and the circular progress component as one of its children. The CSS looks something like:

  
  /* class for div that is the root node of your container component */
.wrapper {
  position: relative
}

 /* class for the backdrop that contains your loader, usually a direct child of the wrapper above */
.backdrop {
  z-index: -1,
  color: #fff,
  position: absolute
}
  

As opposed to using a bumpy ternary expression to choose between two different components, this method allows us to keep both the progress and full existing list on the screen until whatever backend edit (either a new deadline, removed deadline, or edited deadline) is completed.

As far as rendering counts, props will change only twice for your wrapper component on an edit if you have set up your redux store and actions correctly. Once to bring the loader up, and once to both take the loader down and send the updated list through. Meanwhile, the rest of your app is still accessible and not hidden behind a dimmer and loader.