今日は、reactのパフォーマンス改善時に使用されるテクニックの一つであるPureComponentを紹介します。PureComponentとComponentの違いをマスターし、shouldComponentUpdateが何をするのかを理解して、パフォーマンスを改善しましょう!
Contents
PureComponent vs Component 何が違うのか?
まずはReactの公式ドキュメントを確認してみましょう。
React.PureComponent はReact.Componentと似ています。両者の違いは React.Componentが shouldComponentUpdate() を実装していないことに対し、React.PureComponent は props と state を浅く (shallow) 比較することでそれを実装していることです。
出典:https://ja.reactjs.org/docs/react-api.html#reactpurecomponent
まとめると、
React.PureComponent は、 shouldComponentUpdate() を実装しており、React.Componentは、 shouldComponentUpdate() を実装していない。これだけです。
つまり、shouldComponentUpdate()が何をするのか?を理解することが、PureComponent とComponentの違いを理解するための鍵となります。
shouldComponentUpdate()とは何か?
shallow compareをするファンクションです。比較対象(stateやpropsとか)のものが、等しいかどうか浅く確認します。
浅く???聞いたことない。。。という初心者向けに、JavaScriptのリファレンス(参照)の話を交えて、もう少し詳しく解説します。
比較対象がスカラー(整数、論理値、文字列など)の時
こっちは簡単です。その値(value)自体を比較しますので、プログラミング初心者でもすんなり理解できるでしょう。Reactのstateやpropsはオブジェクトですので(例えば、this.state = { isShow: true }
という感じ)、こちらのケースは直接は関係ありません。
単純にスカラーの整数を比較するとは、2
だったものが、3
に変化していないかなど。
比較対象(stateやprops)が複合型(配列、オブジェクトなど)の時
注意すべきはこちらのケースです。Reactのstateやpropsは、オブジェクトですので、今回のpureComponentに関わってくるのもこちらです。
結論から言えば、オブジェクトの場合のshallow compareは、レファレンス(参照)のみチェックし、オブジェクトの中の値(value)はチェックしません。
オブジェクトの例を下記で説明します。
const old = { name: "wata", age: 100};
const young = old; //ここがよろしくない! oldのreferenceが入る。
young.age = 20;
console.log(young); // {name: "wata", age: 20}
console.log(old); // {name: "wata", age: 20} うん??? 100だったような、、、
console.log(old === young); // true
ですので、oldとyoungを比較した時に、trueが返りました。この例では、オブジェクトの中身の値が同じですが、もし異なっていたとしても、レファレンス(参照)が同じであれば、常にtrueが返り、同じものであると見なされます。そうなれば、shouldComponentUpdate()はアップデートする必要はないと判断し、falseを返し、そのコンポーネントはリレンダーされませんので注意してください。
上記のようにデータを操作することをmutateと言います。例のように、意図していないバグが発生しやすいので、mutateは避け、例えば、下記のような方法で、データを更新しましょう.
const old = { name: "wata", age: 100};
const cloned = Object.assign({}, old);
console.log(cloned); // {name: "wata", age: 100}
console.log(old === cloned); // false えっ???
そうなんです。こちらは、オブジェクトの中の値は完全に一致しているのに、falseが返りました。理由は先ほどと同じで、オブジェクトの中の値を比較しているのではなく、レファレンスをチェックしているからです。clonedというオブジェクトは、Object.assign()によって作られた、全く新しいオブジェクトです。よって、oldとclonedという別のオブジェクトを比較しているので(リファレンスが異なる)、falseが返ります。そうなれば、propsに変更があったと判断されshouldComponentUpdate()はtrueを返し、そのコンポーネントはリレンダーされます。
と、ここまでJavaScriptのリファレンスの話を交えて説明してきましたが、これがわかってないと、React.pureComponentの使用で、逆にバグを作ってしまうかもしれませんので、注意しましょう。
PureComponent の使いどころは?
基本的には通常のReact.Componentを利用し、パフォーマンスを最適化するときの一つの手段として、PureComponentを利用を考慮するべきです。
React自体VirtualDOMで差分しかrenderされず性能を保証してくれるため、初心者は気にせずComponentを使用すべきでしょう。パフォーマンスに問題が生じた時に初めてPureComponentを利用を考慮すれば良いでしょう。
つまり、PureComponentは、どこでも使えばいいと言うものではありません。これに関し、FacebookのReact、Reduxの開発者のDan Abramov氏も
どこでも使えばいいのなら、PureComponentがデフォルトになっているはずだけど、実際にはなっていない。
いつもコンポーネントをリレンダーするかどうかのチェックをしていたら、その処理はおそらく無駄でしょう。
と、以前Twitterでコメントしていました。
それでは、PureComponentを使うべきケースとそうでないケースにわけて、以下、まとめます。
PureComponentを使うべきケース
- コンポーネントのpropsやstateが変化しない時。変化しないのに、リレンダーしても意味ないので。よくある具体例を紹介すると、親コンポーネントがアップデートされて、リレンダーされる時に、子のコンポーネントのpropsとstateが変わらないケースです。この時、子のコンポーネントに対してPureComponentを使用することで、無駄なリレンダーを回避でき、パフォーマンス向上を見込めます(後ほど実装例を紹介します)。少し考えてみると当たり前ですが、ルートに近いコンポーネントにPureComponentを実装するほど、パフォーマンスの向上は大きくなります。逆に言えば、最下層のコンポーネントに実装してもリレンダーを回避できるのは、そのコンポーネントのみとなるので、パフォーマンス向上は小さくなります。
- コンポーネントのpropsやstateがimmutableである時。言い換えると、propsやstateをパスするときには、つねに新しく生成したpropsやstateを渡しましょう、ということです。
PureComponentが使うべきでないケース
- propsとstateが常に変化する場合。通常のComponentを使うべきです。shouldComponentUpdate内のshallowEqualにも処理に時間がかかるので、常に変化するのなら、わざわざ余分に比較する必要はありません。
- propsやstateがmutableである時。上記の「比較対象(stateやprops)が複合型(配列、オブジェクトなど)の時」で説明の通りですが、propsやstateがmutableである時には、shouldComponentUpdate()は常にfalseを返し、そのコンポーネントはリレンダーされませんので注意してください。
PureComponent実装時の注意点
上記での「比較対象(stateやprops)が複合型(配列、オブジェクトなど)の時」で説明した通りですが、React.PureComponentのshouldComponentUpdate() は、オブジェクトを浅く比較するのみです。つまり言い換えれば、ネストされた階層の深いオブジェクトで、propsが変更されても、そのコンポーネントはリレンダー(更新)されないと言う問題が生じる可能性がありますので、注意してください。
PureComponent の実装例
それでは、シンプルなメッセージリストの実装でpureComponentの動きを確認します。ちなみに、codesandboxというサイトでdemoを作成しています 。
import React, { Component, PureComponent } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
// ルートコンポーネント
// input formにメッセージを入力するごとに、それをリスト形式で表示するだけのコンポーネント
export default class App extends Component {
constructor() {
super();
this.state = { messages: [] };
// binding
this.getLastMessage = this.getLastMessage.bind(this);
this.onChangeMessage = this.onChangeMessage.bind(this);
}
getLastMessage() {
const lastMessage = this.state.messages[this.state.messages.length - 1];
return lastMessage === undefined ? "" : lastMessage;
}
onChangeMessage(e) {
const messages = [...this.state.messages];
messages.push(e.target.value);
this.setState({ messages: messages });
}
render() {
return (
<div className="App">
<input
type="text"
value={this.getLastMessage()}
onChange={this.onChangeMessage}
style={{ margin: "10px" }}
/>
<MessageList messages={this.state.messages} />
</div>
);
}
}
// メッセージのリストを表示するだけのコンポーネント
class MessageList extends Component {
render() {
return (
<ul>
{this.props.messages.map((m, i) => (
<Message key={i} message={m} />
))}
</ul>
);
}
}
// 一つのメッセージのみを表示するだけのコンポーネント
class Message extends Component {
render() {
console.log("Render;", this.props.message);
return <li style={{ margin: "10px" }}> {this.props.message} </li>;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
consoleのログを見ると、インプットに入力するごとに全てのmessageがリレンダーされているのが確認できます。これは、明らかに無駄なアップデートです。ここで、pureComponentの出番です。
// MessageコンポーネントをpureComponentにする
class Message extends PureComponent {
render() {
console.log("Render;", this.props.message);
return <li style={{ margin: "10px" }}> {this.props.message} </li>;
}}
consoleのログがスッキリし、無駄なリレンダーがなくなったことを確認できます。
参考としたサイト
https://ja.reactjs.org/docs/reconciliation.html