質問
Some tips to speed up your React application.
Update: 

1. Avoid Inline Function Definition in the Render Function

Since functions are objects in JavaScript ({} !== {}), the inline function will always fail the prop diff when React does a diff check. Also, an arrow function will create a new instance of the function on each render if it's used in a JSX property. This might create a lot of work for the garbage collector.

export const SearchWordFilter = ({
  searchWord,
  id,
  changedCallBack,
}: Props): ReactElement => {
  return (
    <InputSearch
      id={id}
      name="searchWord"
      value={searchWord}
      onClear={() => changedCallBack(undefined)}
      onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
        changedCallBack(event.target.value)
      }
    />
  );
};

Instead of defining the inline function for props, you can define the arrow function.

export const SearchWordFilter = ({
  searchWord,
  id,
  changedCallBack,
}: Props): ReactElement => {
  const onSearchWordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    changedCallBack(event.target.value);
  };
  const onSearchWordClear = () => {
    changedCallBack(undefined);
  };

  return (
    <InputSearch
      id={id}
      name="searchWord"
      value={searchWord}
      onClear={onSearchWordClear}
      onChange={onSearchWordChange}
    />
  );
};

2. Avoid Object Literals

Object literals don’t have a persistent memory space, so your component will need to allocate a new location in memory whenever the component re-renders:

function ComponentA() {
  return (
    <div>
      HELLO WORLD
      <ComponentB
        style={{
          color: 'blue',
          background: 'gold',
        }}
      />
    </div>
  );
}

Each time is re-rendered a new object literal has to be “created” in-memory. Additionally, this also means that is actually receiving a different style object. Using memo and PureComponent won’t even prevent re-renders here 😭

This tip doesn’t apply to style props only, but it’s typically where object literals are unwittingly used in React components.

This can be easily fixed by naming the object (outside of the component’s body of course!):

const myStyle = {
  color: 'blue',
  background: 'gold',
};
function ComponentA() {
  return (
    <div>
      HELLO WORLD
      <ComponentB style={myStyle} />
    </div>
  );
}

3. Avoid Frequent Mounting/Unmounting

Many times we’re used to making components disappear using a ternary statement (or something similar):

import React, { Component } from 'react';
import DropdownItems from './DropdownItems';

class Dropdown extends Component {
  state = {
    isOpen: false
  }
  render() {
    <a onClick={this.toggleDropdown}>
      Our Products
      {
        this.state.isOpen
          ? <DropdownItems>
          : null
      }
    </a>
  }
  toggleDropdown = () => {
    this.setState({isOpen: !this.state.isOpen})
  }
}

Since is removed from the DOM it can cause a repaint/reflow by the browser. These can be expensive, especially if it causes other HTML elements to shift around.

In order to mitigate this, it’s advisable to avoid completely unmounting components. Instead, you can use certain strategies like setting the CSS opacity to zero, or setting CSS visibility to “none”. This will keep the component in the DOM, while making it effectively disappear without incurring any performance costs.

4. Avoid using Index as Key for map

You often see indexes being used as a key when rendering a list.

{
  labelNames.map((l, idx) => {
    return (
      <Item key={idx} onClick={() => changeLabel(l.id)}>
        <ButtonWhiteBase text={l.name} isSelected={isSelected(l.id)} />
      </Item>
    );
  });
}

But using the key as the index can show your app incorrect data as it is being used to identify DOM elements. When you push or remove an item from the list, if the key is the same as before, React assumes that the DOM element represents the same component.

It's always advisable to use a unique property as a key, or if your data doesn't have any unique attributes, then you can think of using the shortid module which generates a unique key.

import shortid from 'shortid';
{
  {
    labelNames.map((l) => {
      return (
        <Item key={shortid.generate()} onClick={() => changeLabel(l.id)}>
          <ButtonWhiteBase text={l.name} isSelected={isSelected(l.id)} />
        </Item>
      );
    });
  }
}

However, if the data has a unique property, such as an ID, then it's better to use that property.

{
  labelNames.map((l) => {
    return (
      <Item key={l.id} onClick={() => changeLabel(l.id)}>
        <ButtonWhiteBase text={l.name} isSelected={isSelected(l.id)} />
      </Item>
    );
  });
}

In certain cases, it's completely okay to use the index as the key, but only if below condition holds:

  • The list and items are static
  • The items in the list don't have IDs and the list is never going to be reordered or filtered
  • List is immutable

5. Avoid render before use hook

Render and return must be after all use hook

  • Don't
import React, { ReactElement, useState, useEffect } from 'react';
/* omitted */

function ProductRelatedList({
  productType,
  relatedProducts,
}: Props): ReactElement {
  if (!relatedProducts?.length) {
    return <></>;
  }

  const [showAllItems, setShowAllItems] = useState(false);
  useEffect(() => {
    /* omitted */
  }, []);

  return (
    <>
      <SectionTitle anchorId="relatedProduct" text="同じジャンルのゲーム" />
      <Container>
        <RelatedList />
        /* omitted */
      </Container>
    </>
  );
}
  • Do
import React, { ReactElement, useState, useEffect } from 'react';
/* omitted */

function ProductRelatedList({
  productType,
  relatedProducts,
}: Props): ReactElement {
  const [showAllItems, setShowAllItems] = useState(false);
  useEffect(() => {
    /* omitted */
  }, []);

  if (!relatedProducts?.length) {
    return <></>;
  }

  return (
    <>
      <SectionTitle anchorId="relatedProduct" text="同じジャンルのゲーム" />
      <Container>
        <RelatedList />
        /* omitted */
      </Container>
    </>
  );
}

6. Using Immutable Data Structures

Data immutability is not an architecture or design pattern, it’s an opinionated way of writing code. This forces you to think about how you structure your application data flow. In my opinion, data immutability is a practice that revolves around a strict unidirectional data flow.

Data immutability, which comes from the functional programming world, can be applied to the design of front-end apps. It can have many benefits, such as:

Zero side-effects;
Immutable data objects are simpler to create, test, and use;
Helps prevent temporal coupling;
Easier to track changes.
In the React landscape, we use the notion of Component to maintain the internal state of components, and changes to the state can cause the component to re-render.

React builds and maintains an internal representation of the rendered UI (Virtual DOM). When a component’s props or state changes, React compares the newly returned element with the previously rendered one. When the two are not equal, React will update the DOM. Therefore, we have to be careful when changing the state.

Let’s consider a User List Component:

state = {
  users: [],
};

addNewUser = () => {
  /
   *  OfCourse not correct way to insert
   *  new user in user list
   */
  const users = this.state.users;
  users.push({
    userName: 'robin',
    email: 'email@email.com',
  });
  this.setState({ users: users });
};

The concern here is that we are pushing new users onto the variable users, which is a reference to this.state.users.

Pro Tip: React state should be treated as immutable. We should never mutate this.state directly, as calling setState() afterward may replace the mutation you made.

So what’s wrong with mutating state directly? Let’s say we overwrite shouldComponentUpdate and are checking nextState against this.state to make sure that we only re-render components when changes happen in the state.

 shouldComponentUpdate(nextProps, nextState) {
    if (this.state.users !== nextState.users) {
      return true;
    }
    return false;
  }

Even if changes happen in the user's array, React won’t re-render the UI as it’s the same reference.

The easiest way to avoid this kind of problem is to avoid mutating props or state. So the
addNewUser method could be rewritten using concat:

addNewUser = () => {
  this.setState((state) => ({
    users: state.users.concat({
      timeStamp: new Date(),
      userName: 'robin',
      email: 'email@email.com',
    }),
  }));
};

For handling changes to state or props in React components, we can consider the following immutable approaches:

For arrays: use [].concat or es6 [ ...params]
For objects: use Object.assign({}, ...) or es6 {...params}

7. Use React.Fragments to Avoid Additional HTML Element Wrappers

React.fragments lets you group a list of children without adding an extra node.

function Comments(): ReactElement {
  return (
    <React.Fragment>
      <h1>Comment Title</h1>
      <p>comments</p>
      <p>comment time</p>
    </React.Fragment>
  );
}

But wait! There is the alternate and more concise syntax using React.fragments:

function Comments(): ReactElement {
  return (
    <>
      <h1>Comment Title</h1>
      <p>comments</p>
      <p>comment time</p>
    </>
  );
}

8. Throttling and Debouncing Event Action in JavaScript

Event trigger rate is the number of times an event handler invokes in a given amount of time.

In general, mouse clicks have lower event trigger rates compare to scrolling and mouseover. Higher event trigger rates can sometimes crash your application, but it can be controlled.

Let's discuss some of the techniques.

First, identify the event handler that is doing the expensive work. For example, an XHR request or DOM manipulation that performs UI updates, processes a large amount of data, or perform computation expensive tasks. In these cases, throttling and debouncing techniques can be a savior without making any changes in the event listener.

Throttling

In a nutshell, throttling means delaying function execution. So instead of executing the event handler/function immediately, you’ll be adding a few milliseconds of delay when an event is triggered. This can be used when implementing infinite scrolling, for example. Rather than fetching the next result set as the user is scrolling, you can delay the XHR call.

Another good example of this is Ajax-based instant search. You might not want to hit the server for every key press, so it’s better to throttle until the input field is dormant for a few milliseconds

Throttling can be implemented a number of ways. You can throttle by the number of events triggered or by the delay event handler being executed.

Debouncing

Unlike throttling, debouncing is a technique to prevent the event trigger from being fired too often. If you are using lodash, you can wrap the function you want to call in lodash’s debounce function.

Here’s a demo code for searching comments:

import debouce from 'lodash.debounce';

class SearchComments extends React.Component {
 constructor(props) {
   super(props);
   this.state = { searchQuery: “” };
 }

 setSearchQuery = debounce(e => {
   this.setState({ searchQuery: e.target.value });

   // Fire API call or Comments manipulation on client end side
 }, 1000);

 render() {
   return (
     <div>
       <h1>Search Comments</h1>
       <input type="text" onChange={this.setSearchQuery} />
     </div>
   );
 }
}

If you are not using lodash, you can use the minified debounced function to implement it in JavaScript.

function debounce(a,b,c){var d,e;return function(){function h(){d=null,c||(e=a.apply(f,g))}var f=this,g=arguments;return clearTimeout(d),d=setTimeout(h,b),c&&!d&&(e=a.apply(f,g)),e}}

9. Spreading props on DOM elements

You should avoid spreading properties into a DOM element as it adds unknown HTML attribute, which is unnecessary and a bad practice.

const CommentsText = (props) => {
  return <div {...props}>{props.text}</div>;
};

Instead of spreading props, you can set specific attributes:

const CommentsText = (props) => {
  return <div specificAttr={props.specificAttr}>{props.text}</div>;
};

10. Memoize React Components

Memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. A memoized function is usually faster because if the function is called with the same values as the previous one then instead of executing function logic it would fetch the result from cache.
Let's consider below simple stateless UserDetails React component.

const UserDetails = ({ user, onEdit }) => {
  const { title, full_name, profile_img } = user;

  return (
    <div className="user-detail-wrapper">
      <img src={profile_img} />
      <h4>{full_name}</h4>
      <p>{title}</p>
    </div>
  );
};

Here, all the children in UserDetails are based on props. This stateless component will re-render whenever props changes. If the UserDetails component attribute is less likely to change, then it's a good candidate for using the memoize version of the component:

// React V16.6.0 or greater version
const UserDetails = ({ user, onEdit }) => {
  const { title, full_name, profile_img } = user;

  return (
    <div className="user-detail-wrapper">
      <img src={profile_img} />
      <h4>{full_name}</h4>
      <p>{title}</p>
    </div>
  );
};

export default React.memo(UserDetails);

11. Use Reselect in Redux to Avoid Frequent Re-render

Reselect is a simple selector library for Redux, which can be used for building memorized selectors. You can define selectors as a function, retrieving snippets of the Redux state for React components.

Let's take a look at this code that isn’t using Reselect:

const App = ({ comments, socialDetails }) => (
  <div>
    <CommentsContainer data={comments} />
    <ShareContainer socialDetails={socialDetails} />
  </div>
);

const addStaticPath = (social) => ({
  iconPath: `../../image/${social.iconPath}`,
});

App = connect((state) => {
  return {
    comments: state.comments,
    socialDetails: addStaticPath(state.social),
  };
})(App);

In this code, every time the comments data in state changes, both CommentsContainer and ShareContainer will be re-rendered. This happens even when addStaticPath doesn't make any data changes to socialDetails as addStaticPath will return a new object with a different identity (remember {} != {}). Now, if we rewrite addStaticPath with Reselect, the issue will go away as Reselect will return the last function result until it is passed new inputs.

import { createSelector } from 'reselect';
const socialPathSelector = (state) => state.social;
const addStaticPath = createSelector(socialPathSelector, (social) => ({
  iconPath: `../../image/${social.iconPath}`,
}));

12. CSS Animations Instead of JS Animations

Animations are inevitable for a fluid and pleasurable user experience. There are many ways to implement web animations. Generally speaking, we can create animations three ways:

  • CSS transitions
  • CSS animations
  • JavaScript
    Which one we choose depends on the type of animation we want to add.

When to use CSS-based animation:

To add "one-shot" transitions, like toggling UI elements state;
For smaller, self-contained states for UI elements. For example showing a tooltip or adding a hovering effect for the menu item, etc.
When to use JavaScript-based animations:

When you want to have advanced effects, for example bouncing, stop, pause, rewind, slow down or reverse;
When you need significant control over the animation;
When you need to trigger the animation, like mouseover, click, etc;
When using requestAnimationFrame for visual changes.
Let’s say, for example, you wanted to animate a 4-state div on mouseover. The four stages of div changes the background color from red to blue, blue to green, green to yellow, before rotating 90 degrees. In this case, you would need to use a combination of JavaScript animation and CSS transition to provide better control of the action and state changes.

13. Virtualize Long Lists

List virtualization, or windowing, is a technique to improve performance when rendering a long list of data. This technique only renders a small subset of rows at any given time and can dramatically reduce the time it takes to re-render the components, as well as the number of DOM nodes created.

There are some popular React libraries out there, like react-window and react-virtualized, which provides several reusable components for displaying lists, grids, and tabular data.

14. Using a CDN

A CDN is a great way to deliver static content from your website or mobile application to your audience more quickly and efficiently.

CDN depends on user geographic location. The CDN server closest to a user is known as an “edge server”. When the user requests content from your website, which is served through a CDN, they are connected to edge server and are ensured the best online experience possible.

There are some great CDN providers out there. For example, CloudFront, CloudFlare, Akamai, MaxCDN, Google Cloud CDN, and others.

You can also choose Netlify or Surge.sh to host your static content on CDN. Surge is a free CLI tool that deploys your static projects to production-quality CDN.

References

React Optimizing Performance

21 Performance Optimization Techniques for React Apps

5 Tips to Improve the Performance of Your React Apps

Write a comment
この記事が気に入ったら応援お願いします🙏
3
ツイート
LINE
Developer
Price Rank Dev
I use Next.js (React) and Firebase (Firestore / Auth) for development. We are also developing APIs for Ruby on Rails and GraphQL. Our team members are 6 Vietnamese and Japanese engineers.