DEV Community

Tony Wallace for RedBit Development

Posted on • Originally published at redbitdev.com

Improving Render Performance in React

Improving Render Performance in React

Tony Wallace, November 3 2022

Note: This article begins with a discussion of class components in React. By this point in time you've probably migrated to functional components and hooks. That's great! Functional components and hooks are the way forward for React and we'll discuss them later in this article. We're starting with class components to give some historical perspective, and because you should try to be familiar with both styles. A lot of React apps have been written using class components and it's likely that you'll have to maintain one at some point in your career.

Conditional rendering in class components

If you have worked with class components in React, you may have come across the shouldComponentUpdate lifecycle method. This method runs before each render and accepts the next props and state objects. You can use shouldComponentUpdate to compare the next props and state to the current props and state, and decide whether to allow the component to render. (shouldComponentUpdate returns true by default if you don't override it in your component class, which always allows the component to render.)

In the following example, we have a user profile component that renders a list of the user's skills. Each skill has a name and a description. The description is served as markdown and we need to parse it to HTML before we render it. We can do this with a markdown parser (marked, in this case) and use dangerouslySetInnerHTML to inject the HTML into a <div> element. Note that using dangerouslySetInnerHTML may leave your site vulnerable to XSS attacks. In order to mitigate this risk, we need to sanitize the HTML before we inject it. We'll use DOMPurify for that task.

import React, { Component } from 'react';
import marked from 'marked';
import * as DOMPurify from 'dompurify';

class UserProfile extends Component {
  constructor (props) {
    super(props);
  }

  render () {
    const { userId, name, skills = [] } = this.props;

    return (
      <div className="user-profile">
        <h1>{name} ({userId})</h1>
        <h3>Skills</h3>
        {skills.map((skill) => (
          <div className="skill">
            <h5>{skill.name}</h5>
            <div dangerouslySetInnerHTML={{
              __html: DOMPurify.sanitize(marked.parse(skill.description)),
            }} />
          </div>
        ))}
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

To recap, our process is:

  1. Parse the skill description markdown to HTML
  2. Sanitize the HTML
  3. Inject the HTML into a <div> element

If we think through this process in the context of the component render cycle, we can see how it could have a negative effect on performance:

  1. Perform two potentially expensive tasks in series (markdown parsing and HTML sanitization)
  2. Perform those tasks in a loop, which is also executed in series
  3. Repeat every time the component renders

We can't do much about the first and second issues — we have to parse the markdown, sanitize the resulting HTML and render it — but we can avoid the third issue. Why render if nothing has changed? Here is an updated version of the previous example that uses shouldComponentUpdate to block the component from rendering unless the userId prop has changed:

import React, { Component } from 'react';
import marked from 'marked';
import * as DOMPurify from 'dompurify';

class UserProfile extends Component {
  constructor (props) {
    super(props);
  }

  shouldComponentUpdate (nextProps) {
    return nextProps.userId !== this.props.userId;
  }

  render () {
    const { userId, name, skills = [] } = this.props;

    return (
      <div className="user-profile">
        <h1>{name} ({userId})</h1>
        <h3>Skills</h3>
        {skills.map((skill) => (
          <div className="skill">
            <h5>{skill.name}</h5>
            <div dangerouslySetInnerHTML={{
              __html: DOMPurify.sanitize(marked.parse(skill.description)),
            }} />
          </div>
        ))}
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We're comparing the userId prop because it's a simple equality comparison that will tell us when the user has changed. You might be tempted to try to perform a deep equality comparison of the skills array instead, but that will likely create a worse performance issue than the one we're trying to solve.

shouldComponentUpdate is intended to be used for performance optimization but should be used carefully and sparingly. Blocking a component from rendering can cause bugs in descendant components that can be difficult to fix, and there are usually better ways to improve performance.

Reducing the rendering workload

In the previous example, we tried to mitigate a potential performance problem by blocking a component from rendering. That was the wrong solution to take because it didn't address the real problem. The real problem isn't that we're allowing the component to render when it doesn't have to, it's that we're doing too much work in the render cycle. Therefore, the correct solution is not to block rendering entirely, but to reduce the amount of work we do while rendering.

Option 1: Store preprocessed data on state

One way of reducing the workload is to run the expensive process only when inputs change and store the results on state. This is simple and doesn't interfere with the render cycle. It works equally well in both class and functional components.

Class component

The following example updates our previous examples to preprocess the user's skills and store them on state, instead of reading directly from props in the render cycle. The preprocessing logic has been moved into the processSkills function, which is called in two places: componentDidMount, where it will run before the initial render; and componentDidUpdate, where it will run when the value of the userId prop changes. If the component renders for any other reason, it will continue to use the preprocessed data from state at no additional cost.

import React, { Component } from 'react';
import marked from 'marked';
import * as DOMPurify from 'dompurify';

const processSkills = (skills = []) => {
  return skills.map((skill) => ({
        ...skill,
        htmlDescription: DOMPurify.sanitize(marked.parse(skill.description)),
      }));
};

class UserProfile extends Component {
  constructor (props) {
    super(props);

    this.state = {
      processedSkills: [],
    };
  }

  componentDidMount () {
    this.setState({
      processedSkills: processSkills(this.props.skills),
    });
  }

  componentDidUpdate (prevProps) {
    if (this.props.userId !== prevProps.userId) {
      this.setState({
        processedSkills: processSkills(this.props.skills),
      });
    }
  }

  render () {
    const { userId, name } = this.props;
    const { processedSkills = [] } = this.state;

    return (
      <div className="user-profile">
        <h1>{name} ({userId})</h1>
        <h3>Skills</h3>
        {processedSkills.map((skill) => (
          <div className="skill">
            <h5>{skill.name}</h5>
            <div dangerouslySetInnerHTML={{
              __html: skill.htmlDescription,
            }} />
          </div>
        ))}
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
Functional component (hooks)

The functional component produces the same result as the class component, but is more concise. The useEffect hook calls processSkills with the skills array from props when the value of the userId prop changes. The resulting array is stored on state and used to render the skills list.

import React, { useState, useEffect } from 'react';
import marked from 'marked';
import * as DOMPurify from 'dompurify';

const processSkills = (skills = []) => {
  return skills.map((skill) => ({
        ...skill,
        htmlDescription: DOMPurify.sanitize(marked.parse(skill.description)),
      }));
};

const UserProfile = (props) => {
  const { userId, name, skills = [] } = this.props;
  const [ processedSkills, setProcessedSkills ] = useState([]);

  useEffect(
    () => setProcessedSkills(processSkills(skills)),
    [userId]
  );

      return (
        <div className="user-profile">
          <h1>{name} ({userId})</h1>
          <h3>Skills</h3>
          {processedSkills.map((skill) => (
        <div className="skill">
          <h5>{skill.name}</h5>
          <div dangerouslySetInnerHTML={{
            __html: skill.htmlDescription,
          }} />
        </div>
      ))}
        </div>
      );
}
Enter fullscreen mode Exit fullscreen mode

Option 2: Memoize preprocessed data

Another option is to memoize (not memorize) the results of the process. Memoization is a form of in-memory caching. We're only going to discuss this in the context of functional components where we can use the useMemo hook provided by React, but you may be able to achieve similar results in a class component using a third-party helper.

In this example, the useMemo hook is called on every render. On the initial render and any time the userId prop is updated, useMemo returns the result of calling processSkills with the skills array. If the userId prop hasn't changed since the last render, useMemo returns the previously cached result.

import React, { useMemo } from 'react';
import marked from 'marked';
import * as DOMPurify from 'dompurify';

const processSkills = (skills = []) => {
  return skills.map((skill) => ({
        ...skill,
        htmlDescription: DOMPurify.sanitize(marked.parse(skill.description)),
      }));
};

const UserProfile = (props) => {
  const { userId, name, skills = [] } = this.props;

  const processedSkills = useMemo(
    () => processSkills(skills),
    [userId]
  );

      return (
        <div className="user-profile">
          <h1>{name} ({userId})</h1>
          <h3>Skills</h3>
          {processedSkills.map((skill) => (
        <div className="skill">
          <h5>{skill.name}</h5>
          <div dangerouslySetInnerHTML={{
            __html: skill.htmlDescription,
          }} />
        </div>
      ))}
        </div>
      );
}
Enter fullscreen mode Exit fullscreen mode

Which option should I choose?

If you're working with class components, I suggest preprocessing and storing data on state unless you think you have a strong use case for integrating a memoization helper (e.g. you need memoization throughout your app, not just in one or two places).

In functional components, memoization offers the same benefit as preprocessing and storing the result on state without having to maintain state. However, it isn't guaranteed to be predictable. From the React docs:

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

Storing data on state may be a better choice if the predictability of your performance optimizations is critical to your application. In many, if not most cases it won't be critical, and memoization will likely be the cleanest and simplest way to improve render performance.

Top comments (0)