パフォーマンスの向上
プロジェクトにReactを検討している人がまず質問することの一つに、 Reactが無いバージョンと同等の速度とレスポンスが出るのか、ということが挙げられます。 レスポンスで各stateが変更された際に、コンポーネントのサブツリー全体を再描画させるという考え方は、 パフォーマンスに悪影響を与えるのではないかと疑問を持たれることでしょう。 ReactはUIの更新に必要とされるDOM操作のコストを最小限に抑える賢いテクニックを用います。
無駄なDOM処理(調停)を避ける
Reactは、ブラウザ内で描画されたDOMのサブツリーの記述語(descriptor)である仮想DOMを使用します。 このようにパラレルにDOM構造が保持されることで、 ReactはJavaScriptオブジェクトの処理に比べて遅い処理であるDOMノードの作成と既存のDOMへのアクセスを避けることが出来ます。 コンポーネントのpropsまたはstateが変更される際に、 Reactは実際のDOMの更新が必要なのか否かを、新しい仮想DOMの構築と古いものとの比較から決定します。 これらが等価では無い場合にのみ、Reactは可能な限り少ない変更でDOMの調停(reconcile)を行います。
その上さらに、Reactはコンポーネントのライフサイクル関数であるshouldComponentUpdate
を提供し、
これは再描画処理が開始される(仮想DOMの比較と最終的なDOMの調停)前にトリガされるため、
開発者はこのプロセスを短い循環(?)(short circuit)にすることが可能になります。
この関数のデフォルト実装はtrueを返し、Reactの更新の処理はそのままになります。
shouldComponentUpdate: function(nextProps, nextState) {
return true;
}
Reactはこの関数を頻繁に実行するため、この実装は速くするべきであることに注意して下さい。
仮にあなたは、幾つかのチャットスレッドのあるメッセージアプリケーションの開発に関わっているとします。
スレッドのいずれか一つだけが変更されるとして、
ChatThread
コンポーネント上でshouldComponentUpdate
を実装した場合、
Reactは他のスレッドの描画手順をスキップすることが出来ます。
shouldComponentUpdate: function(nextProps, nextState) {
// TODO : 現在のチャットのスレッドが、一つ前のものと異なるか否か
}
そのため、要約するとReactはshouldComponentUpdate
を使用して処理の循環を短く(short circuit)することで、
仮想DOMの比較によって更新されるべきサブツリーであっても、
その調停を必要とするDOMのサブツリーのコストの高いDOM操作実行を避けることになります。(翻訳に自信なし)
動作内のshouldComponentUpdate
下記の図はコンポーネントのサブツリーになります。
それぞれの円はshouldComponentUpdate
が返す真偽値と仮想DOMに評価されるか否かを示します。
また円の色は、調停の必要があるか否かを示します。
上記の例では、C2に根付くサブツリーのshouldComponentUpdate
がfalseを返すため、
Reactは新しい仮想DOMを生成する必要は無く、そのためDOMの調停も必要ありませんでした。
ReactはC4とC5に対しては、shouldComponentUpdate
を実行する必要すら無かった事に注意して下さい。
C1とC3のshouldComponentUpdate
はtrueを返したので、Reactは階層を下って要素(ツリーの葉に当たる部分)を調べます。
C6はtrueを返し、仮想DOMとの比較が等価では無かったため、DOMの調停が必要でした。
最後は興味深いC8のケースについてです。
Reactはこのノードに対して仮想DOMの処理をする必要がありましたが、
その結果古いものと等価だったため、そのDOMの調停は必要ありませんでした。
Reactは必然的にC6だけのDOM変更が必要だったことに注目して下さい。
C8は仮想DOMとの比較で変更の必要は無いと判断され、
C2のサブツリーとC7に至っては、shouldComponentUpdate
で必要無いと判断されることで、
仮想DOMの処理すら必要ありませんでした。
これらを踏まえ、我々はどのようにしてshouldComponentUpdate
を実装するべきなのでしょうか?
例えば、あなたは下記のように文字列値だけを描画するコンポーネントを持つものとします。
React.createClass({
propTypes: {
value: React.PropTypes.string.isRequired
},
render: function() {
return <div>this.props.value</div>;
}
});
shouldComponentUpdate
は下記のように簡単に実装することが可能です。
shouldComponentUpdate: function(nextProps, nextState) {
return this.props.value !== nextProps.value;
}
ここまでは問題ないでしょう。 このような単純なprops/state構造の扱いは簡単です。 浅い等式とコンポーネントへのミックスインを基にした実装を、一般化することも可能です。 実際にReactは既にそのような実装(PureRenderMixin)を提供しています。
ただし、もしコンポーネントのpropsまたはstateが可変のデータ構造だったらどうでしょうか?
コンポーネントが受け取るpropが、'bar'
のような文字列の代わりに、
{ foo: 'bar' }
のような文字列を含むJavaScriptオブジェクトであるとします。
React.createClass({
propTypes: {
value: React.PropTypes.object.isRequired
},
render: function() {
return <div>this.props.value.foo</div>;
}
});
先ほどのshouldComponentUpdate
の実装は、期待したとおりに動作してくれません。
// this.props.valueは{ foo: 'bar' }と仮定し、
// nextProps.value も{ foo: 'bar' }と仮定したとして、
// この参照は、this.props.valueとは異なると判定されます。
this.props.value !== nextProps.value; // true
この問題は、実際にプロパティが変更されなかった場合にshouldComponentUpdate
がtrueを返すことにあります。
これを修正するために、代わりに次のように実装します。
shouldComponentUpdate: function(nextProps, nextState) {
return this.props.value.foo !== nextProps.value.foo;
}
正しく変更されたことを認識するためには、深い階層での比較が必要になるということです。 パフォーマンスの観点からは、このアプローチは非常にコストが掛かるものになります。 各モデルに深い階層での異なる等式のコードを各必要があるため、スケールされません。 更にもしオブジェクト参照の管理を注意深く行わなければ、正しく動作しない可能性があります。
親コンポーネントに使用されるコンポーネントの例で考えてみましょう。
React.createClass({
getInitialState: function() {
return { value: { foo: 'bar' } };
},
onClick: function() {
var value = this.state.value;
value.foo += 'bar'; // アンチパターン!
this.setState({ value: value });
},
render: function() {
return (
<div>
<InnerComponent value={this.state.value} />
<a onClick={this.onClick}>Click me</a>
</div>
);
}
});
内部コンポーネントが初めて描画されると、値プロパティとして{ foo: 'bar' }
を持ちます。
ユーザーがアンカーをクリックすると、親コンポーネントのstateは{ value: { foo: 'barbar' } }
に更新され、
内部コンポーネントの再描画処理のトリガは、プロパティの新しい値として{ foo: 'barbar' }
を受け取ります。
この問題は、親と内部コンポーネントは同じオブジェクト参照を共有するため、
onClick
関数の2行目でオブジェクトが変更された際に、内部コンポーネントのプロパティが変更されるということです。
そのため、再描画処理が開始され、shouldComponentUpdate
が実行される際に、
this.props.value.foo
はnextProps.value.foo
と等価になります。
何故なら、実際にthis.props.value
の参照するオブジェクトは、nextProps.value
のそれと同じだからです。
従って、プロパティの変更と短い循環(short circuit)の再描画処理を見逃すことになり、 UIは'bar'から'barbar'へ更新されません。
救済措置のためのImmutable-js
Immutable-jsは、 Lee Byron氏によって書かれたJavaScriptのコレクションのライブラリで、Facebookが最近オープンソース化したものです。 これは、構造の共有(structural sharing)を介して、 不変の永続性(immutable persistent)コレクションを提供してくれます。 これらの用語が何を意味するのかを確認してみましょう。
不変性(Immutability)は、変更の追跡を容易にしてくれます。 変更があれば常に新しいオブジェクトが作成されることになるため、 変更されたオブジェクトの参照を調べるだけで良いことになります。 例えば、通常のJavaScriptコードでは次のようになります。
var x = { foo: "bar" };
var y = x;
y.foo = "baz";
x === y; // true
y
が編集されましたが、x
と同じオブジェクトを参照しているため、この比較はtrue
を返します。
このコードは、immutable-jsであれば次のように書くことが出来ます。
var SomeRecord = Immutable.Record({ foo: null });
var x = new SomeRecord({ foo: 'bar' });
var y = x.set('foo', 'baz');
x === y; // false
このケースでは、x
が変更された際に新しい参照が返されるため、
我々は安全にx
が変更されたとみなすことが出来ます。
変更の追跡を可能とする別の方法に、setterによるフラグ設定を持つことによるダーティ・チェック(dirty checking)があります。 このアプローチの問題点は、setterと多くの追加コード、またはクラスへの何らかの機能追加が強制されてしまうことです。 あるいは、そのオブジェクトを変更直前にディープ・コピーし、変更されたか否かを深い階層まで比較(deep compare)する方法があります。 このアプローチの問題は、ディープ・コピーも深い比較までの比較も、両方ともコストの掛かる処理であるということです。
つまり変更不可(Immutable)なデータ構造は、
shouldComponentUpdate
の実装が必要になるオブジェクトの変更の追跡に掛かるコストと冗長性を減らす手段を提供してくれるということです。
そのため、もしimmutable-jsによって提供される抽象化を使用してpropsとstateの属性を作るのであれば、
PureRenderMixin
の使用と優れたパフォーマンスを得ることが可能になるでしょう。
Immutable-jsとFlux
もしFluxを使用するのであれば、 immutable-jsを使用してストア(store)を書くことから始めるべきでしょう。 immutable-jsの完全なAPIを確認してみてください。
変更不可(Immutable)なデータ構造を使用して、スレッドを作る一例を見てみましょう。
始めに、作ろうとしている各実体に対してRecord
を定義します。
このRecordこそ変更不可なデータの入れ物であり、特定のフィールドに設定される値を保持します。
var User = Immutable.Record({
id: undefined,
name: undefined,
email: undefined
});
var Message = Immutable.Record({
timestamp: new Date(),
sender: undefined,
text: ''
});
Record
関数は、オブジェクトが持つフィールドとそのデフォルト値が定義されたオブジェクトを受け取ります。
messagesストア(store)は2つのリストを使用してユーザーとメッセージの追跡を保持します。
this.users = Immutable.List();
this.messages = Immutable.List();
それぞれの型を処理する関数の実装は、非常に分かりやすいものにすべきです。 例えば、積まれた新しいメッセージをストアが認識した際に、 新しいRecordを作成してそれをメッセージリストに追加するだけで済むようになります。
this.messages = this.messages.push(new Message({
timestamp: payload.timestamp,
sender: payload.sender,
text: payload.text
});
データ構造が変更不可(Immutable)であるため、
this.message
へのpush
関数の結果を割り当てる必要があることに注意してください。
コンポーネントのstateを保持するためのデータ構造にもimmutable-jsを使用する場合、
全てのコンポーネントと再描画処理の短い循環(short circuit)をPureRenderMixin
に混ぜることも可能です。