banner
他山之石

他山之石

高性能のReactコードを書く

この記事では、React コードを書くためのいくつかの提案をまとめており、React のレンダリングメカニズムを理解するのに役立ちます。主に不必要な再レンダリングを減らすことで React のパフォーマンスを向上させることを目的としています。

image

React を使用して開発した経験を振り返ると、もうすぐ 2 年になります。高性能の React コードを書く方法やパフォーマンスの最適化についての記事を書きたいと思っていましたので、高性能の React コードを書くための実践をまとめました。これがあなたの React の理解を深める手助けになるかもしれません。

React のパフォーマンスに影響を与える主な要因#

React はユーザーインターフェースの構築に特化しており、内部で VirtualDOM と Diff アルゴリズムを使用してビューの更新を実現しています。元々高いパフォーマンスを持っています。コンポーネントの state や props が変更されると、React は再レンダリングを行い、ビューを更新します。レンダリングプロセスでは、React は JSX 構文を仮想 DOM に変換し、Diff アルゴリズムを使用して新旧の DOM の変化を比較し、ビューを部分的に更新します。想像してみてください。レンダリングが実行された後、JSX が仮想 DOM に変換され、Diff が行われ、最終的に新旧の DOM が同じであることが判明した場合、ビューは更新されません。これにより、不必要なレンダリングが発生し、パフォーマンスの浪費が生じます。したがって、不必要なレンダリングを減らすことが React のパフォーマンスを最適化する主な要因となります。

state はビューにのみ関連する#

React コンポーネントを書くとき、変数を使用することは間違いありません。変数は this に保存することも、state に保存することも、グローバル変数を使用することもできます。では、いつ state を使用すべきでしょうか?答えは、ビューに関連する変数のみを state に置き、ビューに関連しないものは this に置くべきです。ビューの更新に関係のない変数を state に置き、その変数が頻繁に更新される場合、毎回更新するたびに再レンダリングが必要になり、パフォーマンスの浪費を引き起こします。this を使用して保存する方が明らかに良い選択です。

shouldComponentUpdate#

以下のコードを考えてみましょう:

export default class Home extends Component {
  state = {
    count: 0
  }

  add = () => {
    this.setState({count: 0});
  }
  
  render() {
    console.log('render');
    return (
      <div className="App">
        <div onClick={this.add}>+++</div>
        <p className="App-intro">
          {this.state.count}
        </p>
      </div>
    );
  }
}

+++ をクリックして add がトリガーされると、setState が state を更新しますが、新旧の state は全く同じです。React は Component コンポーネント内で最適化を行わず、不必要なレンダリングが実行されます。この場合、手動で最適化を行う必要があります。state または props が更新された後、React コンポーネントのライフサイクル shouldComponentUpdate がトリガーされ、shouldComponentUpdate 内で新旧の state と props を比較し、コンポーネントが再レンダリングする必要があるかどうかを判断できます。

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

デフォルトでは shouldComponentUpdate は true を返し、常に再レンダリングされます。この場合、shouldComponentUpdate を使用して比較した後、false を返すと、React は再レンダリングを行いません。

state が文字列などの基本型の場合、shouldComponentUpdate は問題なく対応できますが、state がオブジェクトや配列などの参照型の場合はどうでしょうか?参照型の場合は、再帰的に比較する必要があります。または、JSON.stringify (nextState.obj) === JSON.stringify (this.state.obj) を使用して比較することもできますが、オブジェクトが深くネストされている場合や関数、正規表現などの型の場合、shouldComponentUpdate はその効果を失います。

PureComponent#

React.PureComponent は React.Component とほぼ完全に同じですが、React.PureComponent は prop と state の浅い比較を通じて shouldComponentUpdate () を実現します。簡単に言えば、PureComponent は内部で shouldComponentUpdate を実装しており、私たちが比較コードを書く必要はありません。React.PureComponent の shouldComponentUpdate () はオブジェクトに対してのみ浅い比較を行います。オブジェクトが複雑なデータ構造を含む場合、深層のデータの不一致により誤った否定判断を引き起こす可能性があります(オブジェクトの深層データが変更されてもビューが更新されないことを示します)。テストによると、PureComponent を使用することで shouldComponentUpdate よりもわずかにパフォーマンスが向上します。

StateLessComponent#

無状態コンポーネント(StateLessComponent)は関数型コンポーネントとも呼ばれます。

const Text = ({ children = 'Hello World!' }) =>
  <p>{children}</p>

関数型コンポーネントは class 宣言がなく、ライフサイクルもなく、render メソッドのみを使用して実装され、コンポーネントのインスタンス化プロセスが存在しないため、余分なメモリを割り当てる必要がなく、レンダリングパフォーマンスが向上します。

アロー関数と bind#

コンポーネントにイベントハンドラをバインドする際、state に正常にアクセスできるようにするために、通常は bind またはアロー関数を使用します。

<Button onClick={this.update.bind(this)} />

または

<Button onClick={() => { console.log("Click"); }} />

上記の状況では、bind は毎回新しい関数を返し、アロー関数は毎回 render 時に再割り当てされます(bind を使用する方法と同様です)。コンポーネントにとって、毎回バインドされるのは新しい関数であり、Button コンポーネントが props を比較する際に前後の props が異なると見なされ、再レンダリングが発生します。推奨される書き方は 2 つあります:

update = () => {
    //
}
<Button onClick={this.update} />

または

constructor(props) {
    super(props);
    this.update = this.update.bind(this);
}

update() {
    //
}
<Button onClick={this.update} />

インラインスタイル#

コンポーネントにスタイルを設定する際、次のようなコードを書くことがあります:

<Button style={{width: 100, marginTop: 50 }} />

この書き方では、毎回 render 時に style が新しいオブジェクトを返します。参照型は参照アドレスを比較するため、毎回の結果は不等しくなり、再レンダリングを引き起こします。推奨される書き方は className を使用することです。

<Button className="btn" />

属性の渡し方と []#

コンポーネントにデフォルト値を設定する際:

<RadioGroup options={this.props.options || []} />

もし毎回 this.props.options の値が null であれば、に渡されるのは常にリテラル配列 [] であることを意味しますが、リテラル配列と new Array () の効果は同じで、常に新しいインスタンスが生成されます。したがって、見た目上は毎回コンポーネントに渡されるのは同じ空の配列ですが、実際にはコンポーネントにとっては毎回新しい属性となり、レンダリングを引き起こします。したがって、一般的な値を変数として保存するのが正しい方法です:

const DEFAULT_OPTIONS = [];

<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />
注意:ここでのDEFAULT_OPTIONSはrender内に書いてはいけません。render内に書くと、毎回新しい配列が生成され、再レンダリングが発生します。正しい方法はコンポーネントの外に置くか、this.DEFAULT_OPTIONS = []を使用することです。

map と key#

React では、リストをレンダリングするために map を頻繁に使用するでしょう。各リスト要素に key 属性を設定し忘れると、React はコンソールに「リストアイテムには key を提供する必要があります」と警告します。実際、Keys は DOM 内の特定の要素が追加または削除されるときに、React がどの要素が変更されたかを識別するのに役立ちます。したがって、配列内の各要素に確定的な識別子を与えるべきです。

要素の key は、その要素がリスト内で持つユニークな文字列が最適です。通常、データからの id を要素の key として使用します:

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

要素に確定的な id がない場合、その序列番号インデックスを key として使用できます:

const todoItems = todos.map((todo, index) =>
  // アイテムに安定したIDがない場合のみ行います
  <li key={index}>
    {todo.text}
  </li>
);

todoItems 内のデータが変更されると、インデックスを key として使用したリストはすべて再レンダリングされます。したがって、ソートにインデックスを使用することはお勧めできません。なぜなら、これによりレンダリングが非常に遅くなるからです。

render 内での変数の実行を減らす#

コンポーネントの state や props が変更されると、render が実行されることがわかっています。したがって、render 内で変数や関数を実行すると、レンダリングパフォーマンスに影響を与える可能性があるため、render 内での変数や関数の実行を減らすべきです。

props と state の簡素化#

上記からわかるように、props と state が参照型の場合、React 内部では props と state の浅い比較のみが行われます。したがって、props と state はできるだけ基本データ型に設定し、オブジェクトのネストレベルを深くしすぎず、props のパラメータ数を減らし、コンポーネントの props と state オブジェクトのデータをできるだけフラットにすることで、React のレンダリングパフォーマンスを最適化できる可能性があります。

まとめ#

この記事では、React コードを書くための一連の原則について議論しました。主に再レンダリングを減らすことで React コードのパフォーマンスを向上させることを目的としています。パフォーマンスの最適化は常に持続的なテーマであり、immutable.js のような不変データ構造を使用したり、redux 内の複雑な計算をキャッシュするために reselect を使用したり、コンポーネントをできるだけ子コンポーネントに分割することなど、さまざまな観点から考慮することができます。次回お会いしましょう。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。