IT人材のためのキャリアライフスタイルマガジン

React製社内ツールの高速化事例(有田夏洋)|TechHub React勉強会

入門

弊社Brandibg Engineer主催イベントにて、エンジニア3名に登壇していただき、運用して感じたことを発表していただきました。当記事は、登壇していただいたうちの1人、IndeedTokyo 有田氏の発表を書き起こしたものになります。

更新日時:

React製社内ツールの高速化事例

こんにちは、Indeedの有田です。 「React製社内ツールの高速化事例」というテーマでお話させていただきます。

自己紹介

勤めている会社は、恵比寿にある「Indeed Tokyo」です。 「Indeed」では、ネット上でクロールした求人情報を提供するサービスを行っています。私のチームは、Indeedの中の「CompanyPages」と「Company Data」というところに属しておりまして、会社に関する情報を公開しています。 普段書いている言語はJavaとJavaScriptです。

React製社内ツール

Company PagesのほうではReactを使っていません。 一般に公開されるサイトなので検索エンジン対策(SEO)が必須なんですが、「Reactはサーバーサイドレンダリングが遅い」ということを、社内の実験で明らかにしまして、Reactは使われずJavaSoyで普段書かれています。 社内向けのツールは一般に公開しないため、検索エンジン対策を考える必要がありませんのでReactを使っています。社内向けツールは、「会社の情報を一覧表示して、その情報を操作する」というツールです。

React製社内ツールのイメージをざっくりと表で用意しました(スライド4)。一行一行が会社になります。 ドロップダウンを押すことで、その会社情報に対する操作を設定することができます。SubmitActionsを押すと、選択されたアクションが送信されます。

次にコードの話に移ります。 簡単に分類すると、二つのクラスに分かれていて、1つは通信などを行う「Tool」のクラスと、もう1つはToolの中にある「Table」のクラスです。Toolのクラスの中に、表として表示するlistが入ります。 Toolの中でTableを呼んで、Tableにlistを渡します。そして、Tableの中で渡されたlistをrenderしています。

元々50行だけの情報を表示していたのですが、1000行を表示する変更の要望があり、その際に2つの速度問題が起こりました。 1つ目の問題は、1000行を表示したときに、セルのrenderを呼ぶ回数が多くなり、ドロップダウンの選択に1秒もかかってしまうことです。 もう1つの問題は、「Toolの下にTableがあり、ToolからTableにlistが渡される」という形になっているため、親のstateが変わって親がrenderされると、子も自動的にrenderされてしまうことです。明らかにlistのデータが不変でも、renderを行うため、動作が遅くなっていました。

1つ目の問題に対しては、画面に表示されていないセルはrenderを呼ばない、Facebook製の「FixedDataTable」というライブラリで対応しました。 元々「FixedDataTable」は使われていましたが、高さが無限に設定されていたため、何の意味もありませんでした。高さを設定することで、表示している範囲だけを描画するようになり、セルのrenderが呼ばれる回数を減らせました。

続いて二つ目の問題の解決方法について説明します。まずはrenderの振り返りをします。 Reactでは、renderがstateに基いて現在のVirtual DOMを返すと、前回のVirtualDOMとの差分をフレームワーク側が計算してくれます。DOMの再描画が一番ボトルネックなので、計算を減らして、重くなりにくくしてくれます。 今回、「renderの呼び出し自体が重い」という問題が起こっていました。この問題を解決するために、shouldComponentUpdateという関数を使いました。shouldComponentUpdateは、次の状態のpropsとstateが渡されて、現在のpropsとstateに変化があったかを記述します。 変化していなければfalseを返します。render自体が呼び出されなくなるので、速度が高速化されます。

しかし、どうやって書くかという問題があり、これに対して3つの案がありました。

案1: PureRenderMixin

案1です。 PureRenderMixinという、あるComponentがpureであるときに使えるMixinがあります。「pureである」ということは、「入力が同じなら出力も同じ」ということを意味しています。つまり、stateとpropsのみに依存して出力が決まるというのが、pureなComponentです。 PureRenderMixinはstateとpropsとそれぞれのフィールドを参照比較するものです。

実際には正しくありませんが、概念的にはこのようになります(スライド10)。 nextPropsのkeyそれぞれを現在のpropsと比較して、異なっていればupdateをしなければいけません。stateもpropsと同様で異なっていればupdateします。

PureRenderMixinを使えば万々歳と思いきや、つらいコードがたくさんあります。 例えばこういった更新のされ方がありました(スライド11)。listがstateの一つのフィールドで、深いところを破壊的に更新すると、listにthis.state.listをセットすることが行われます。 このことによってイコールで比較する際に、現在のlistと次のlistが、参照的に同じものになってしまい、いくらstateを更新しても変更されない事態が発生します。ですから、PureRenderMixinは使えません。

案2: deep equal

案2が「deep equal」です。 参照比較に対して、渡されたオブジェクトを再帰的に比較すればいいのではないかと思いました。しかし、現在のlistも次のlistも破壊的に変更されていくので、deepequal は意味がありません。

そこで、こんなライブラリが欲しいわけです。 深いところを変更したら参照が変わってほしい 再帰的に全てのフィールドをコピーするのは大変そう listへの破壊的操作や同じものをsetしなおすことを禁止したい

案3: Immutable.js

最後の案3は「Immutable.js」です。Immutable.jsを使うことで、さきほどの3つを実現できました。 「Immutable.js」は、「変更可能な値」と「その値の一部を変更した新たな値」を効率的に生成するメソッド郡のライブラリです。 一度作られたmap1はいつまでも変わらず、メソッド(スライド13-コード2行目のmap1.set(‘b’,50)など)は新たなImmutableな値を作るというものです。

例えば、fromJSメソッドを使うと、再帰的にJSのオブジェクトや配列をImmutableに変換してくれます。pushメソッドは、listの最後にエレメントを追加した新たなlistを返します。これをsetStateメソッドで新しいlistに設定します。 このように、Immutable.jsでは便利なメソッドが用意されています。

破壊的な変更をした場合には、list自体の参照が変わるので、shouldComponentUpdateは単にlistが同じかどうかで、更新するかどうかの判定をすることができます。

結果としては、変更されていない場合は、極力renderを省略でき、高速に動作するようになりました。 「極力renderを省略できる」と表現していて、なぜ「必ず省略できる」ではないのかと言いますと、破壊的な変更を何度もした結果、元に戻ったとき、同じとは限らないからです。 そして、listが変更された場合、必ずrenderがされるようになっています。ですから、「stateを変えたのにおかしい」、「表示が変わらない」ということが発生しません。

おまけ

おまけとして、ReduxとImmutable.jsについてお話します。 私たちのチームではReduxを使っていなくて、Reactのまま使っています。おそらくReduxを使うと、メリットもあると思いますが、優先度が少し低いので使っていません。 ただ、何らかの新しいアクションが来た時に新しいstateをつける「Reducer」に、「stateに対して破壊的な変更をしてはならない」というルールがあるので、そのルールをうっかり破ってしまわない保証としてReduxが使えそうです。これで変なバグが起きなくなりそうですね。

まとめ

最後にまとめです。 React製社内ツールの速度問題と解決までのアプローチを説明してきました。高速化の結果をまとめると、次の3つになります。 1つ目は、基盤のテーブルにFixedDataTableを使い、セルのrender呼び出し数を減らせました。 2つ目は、shouldComponentUpdateで速度のチューニングを行い、renderを呼ぶ回数を抑えられました。 最後に、複雑な深いstateにはImmutable.jsを使うことで、高速化ができました。 以上がReactの高速化事例になります。ご清聴ありがとうございました。

関連タグ

アクセスランキング