あなたのReactアプリは少し遅く感じていますか? 表示される可能性があるため、Chrome DevToolsで「ペイントフラッシュ」をオンにすることを恐れていますか? これらの5つのパフォーマンスのヒントを試してみてください!

この記事には、React開発のための5つのパフォーマンスのヒントが含まれています。 この目次を使用して、この記事内をすばやく移動できます。

ヒントにジャンプ

memoおよびPureComponentを使用します

以下のこの単純なReactアプリを検討してください。 props.propAだけが値を変更した場合、<ComponentB>が再レンダリングされると思いますか?

import React from 'react';

const MyApp = (props) => {
  return (
    <div>
      <ComponentA propA={props.propA}/>
      <ComponentB propB={props.propB}/>
    </div>
  );
};

const ComponentA = (props) => {
  return <div>{props.propA}</div>
};

const ComponentB = (props) => {
  return <div>{props.propB}</div>
};

答えはイエスです! これは、MyAppが実際に再評価(または再レンダリング😏)され、<ComponentB>がそこにあるためです。 そのため、独自の小道具は変更されていませんが、その親コンポーネントによって再レンダリングされます。

この概念は、クラスベースのReactコンポーネントのrenderメソッドにも適用されます。

Reactの作成者は、これが必ずしも望ましい結果であるとは限らず、再レンダリングする前に古い小道具と新しい小道具を比較するだけで簡単にパフォーマンスが向上する…これが本質的にReactです。 memoReact.PureComponentは、そのように設計されています。


memo を機能コンポーネントで使用してみましょう(クラスベースのコンポーネントについては後で説明します)。

import React, { memo } from 'react';

// 🙅‍♀️
const ComponentB = (props) => {
  return <div>{props.propB}</div>
};

// 🙆‍♂️
const ComponentB = memo((props) => {
  return <div>{props.propB}</div>
});

それでおしまい! <ComponentB>memo()関数でラップする必要があります。 これで、親が再レンダリングする回数に関係なく、propBが実際に値を変更した場合にのみ再レンダリングされます。


PureComponentを見てみましょう。 基本的にmemoと同等ですが、クラスベースのコンポーネント用です。

import React, { Component, PureComponent } from 'react';

// 🙅‍♀️
class ComponentB extends Component {
  render() {
    return <div>{this.props.propB}</div>
  }
}

// 🙆‍♂️
class ComponentB extends PureComponent {
  render() {
    return <div>{this.props.propB}</div>
  }
}

これらのパフォーマンスの向上はほとんど簡単すぎます! Reactコンポーネントに、過度の再レンダリングに対するこれらの内部セーフガードが自動的に含まれないのはなぜか疑問に思われるかもしれません。

実際には、memoPureComponentには隠れたコストがあります。 これらのヘルパーは古い/新しい小道具を比較するので、これは実際にはそれ自体のパフォーマンスのボトルネックになる可能性があります。 たとえば、小道具が非常に大きい場合、またはReactコンポーネントを小道具として渡す場合、古い小道具と新しい小道具の比較にはコストがかかる可能性があります。

プログラミングの世界で銀の弾丸はまれです! そして、 memo /PureComponentも例外ではありません。 あなたは間違いなくそれらを測定された、思慮深い方法で試乗したいと思うでしょう。 場合によっては、計算量をどれだけ節約できるかを驚かせることがあります。

React Hooksについては、不要な計算作業を防ぐための同様の方法としてuseMemoをチェックしてください。

匿名関数を避ける

コンポーネントの本体内にある関数は、通常、イベントハンドラーまたはコールバックです。 多くの場合、匿名関数を使用したくなるかもしれません。

import React from 'react';

function Foo() {
  return (
    <button onClick={() => console.log('boop')}> // 🙅‍♀️
      BOOP
    </button>
  );
}

匿名関数には(const/let/varを介して)識別子が割り当てられないため、この関数コンポーネントが必然的に再度レンダリングされるたびに永続化されません。 これにより、JavaScriptは、「名前付き関数」を使用するときに単一のメモリを1回だけ割り当てるのではなく、このコンポーネントが再レンダリングされるたびに新しいメモリを割り当てます。

import React, { useCallback } from 'react';

// Variation 1: naming and placing handler outside the component 
const handleClick = () => console.log('boop');
function Foo() {
  return (
    <button onClick={handleClick}>  // 🙆‍♂️
      BOOP
    </button>
  );
}

// Variation 2: "useCallback"
function Foo() {
  const handleClick = useCallback(() => console.log('boop'), []);
  return (
    <button onClick={handleClick}>  // 🙆‍♂️
      BOOP
    </button>
  );
}

useCallback は無名関数の落とし穴を回避する別の方法ですが、前に説明したReact.memoに伴う同様のトレードオフがあります。

クラスベースのコンポーネントを使用すると、ソリューションは非常に簡単で、実際には欠点はありません。 これは、Reactでハンドラーを定義するための推奨される方法です。

import React from 'react';

class Foo extends Component {
  render() {
    return (
      <button onClick={() => console.log('boop')}>  {/* 🙅‍♀️ */}
        BOOP
      </button>
    );
  }
}

class Foo extends Component {
  render() {
    return (
      <button onClick={this.handleClick}>  {/* 🙆‍♂️ */}
        BOOP
      </button>
    );
  }
  handleClick = () => {  // this anonymous function is fine used like this
    console.log('boop');
  }
}

オブジェクトリテラルを避ける

このパフォーマンスのヒントは、無名関数に関する前のセクションと同様です。 オブジェクトリテラルには永続的なメモリスペースがないため、コンポーネントが再レンダリングするたびに、コンポーネントはメモリ内に新しい場所を割り当てる必要があります。

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

function ComponentB(props) {
  return (
    <div style={this.props.style}>
      TOP OF THE MORNING TO YA
    </div>
  )
}

<ComponentA>が再レンダリングされるたびに、新しいオブジェクトリテラルをメモリ内に「作成」する必要があります。 さらに、これは、<ComponentB>が実際に別のstyleオブジェクトを受信していることも意味します。 memoPureComponentを使用しても、ここでの再レンダリングを防ぐことはできません😭

このヒントはstyleプロップにのみ適用されませんが、通常、オブジェクトリテラルがReactコンポーネントで無意識のうちに使用される場所です。

これは、オブジェクトに名前を付けることで簡単に修正できます(もちろんコンポーネントの本体の外にあります!):

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

function ComponentB(props) {
  return (
    <div style={this.props.style}>
      TOP OF THE MORNING TO YA
    </div>
  )
}

React.lazyReact.Suspenseを使用します

Reactアプリを高速化することの一部は、コード分割によって実現できます。 この機能は、React.lazyおよびReact.Suspenseを使用してReactv16に導入されました。

ご存じない方もいらっしゃると思いますが、コード分割の概念は、JavaScriptクライアントソース(Reactアプリコードなど)を小さなチャンクに分割し、これらのチャンクを遅延的にロードすることです。 コードを分割しないと、単一のバンドルが非常に大きくなる可能性があります。

- bundle.js (10MB!)

コード分割を使用すると、バンドルに対する最初のネットワーク要求が大幅に小さくなる可能性があります。

- bundle-1.js (5MB)
- bundle-2.js (3MB)
- bundle-3.js (2MB)

最初のネットワークリクエストは5MBをダウンロードするだけでよく、エンドユーザーにとって興味深いものを表示し始めることができます。 最初にヘッダーとフッターのみが必要なブログWebサイトを想像してみてください。 それが読み込まれると、実際のブログ投稿を含む2番目のバンドルの要求が開始されます。 これは、コード分割が便利な基本的な例です。 👏👏👏

これをReactでどのようにコード分割しますか?

Create React App を使用している場合は、コード分割用に既に構成されているため、React.lazyReact.Suspenseを使用できます。ゲート! 自分でwebpackを設定している場合は、のようになります

lazySuspenseが実装されている簡単な例を次に示します。

import React, { lazy, Suspense } from 'react';
import Header from './Header';
import Footer from './Footer';
const BlogPosts = React.lazy(() => import('./BlogPosts'));

function MyBlog() {
  return (
    <div>
      <Header>
      <Suspense fallback={<div>Loading...</div>}>
        <BlogPosts />
      </Suspense>
      <Footer>
    </div>
  );
}

fallbackプロップに注目してください。 これは、2番目のバンドルチャンクがロードされている間すぐにユーザーに表示されます(例:<BlogPosts>)。

ReactSuspenseを使用したコード分割に関するこのすばらしい記事をご覧ください🐊

頻繁な取り付け/取り外しは避けてください

多くの場合、3項ステートメント(または同様のもの)を使用してコンポーネントを非表示にすることに慣れています。

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})
  }
}

<DropdownItems>がDOMから削除されているため、ブラウザによって repaint /reflowが発生する可能性があります。 これらは、特に他のHTML要素が移動する場合は、コストがかかる可能性があります。

これを軽減するために、コンポーネントを完全にアンマウントしないようにすることをお勧めします。 代わりに、CSS 不透明度をゼロに設定したり、CSS 可視性を「なし」に設定したりするなど、特定の戦略を使用できます。 これにより、コンポーネントがDOMに保持されると同時に、パフォーマンスコストを発生させることなくコンポーネントが効果的に消滅します。