はじめに
こんにちは、カミナシ社でソフトウェアエンジニアをやっている浦岡です。
我々は「ノーコードでユーザー独自の現場アプリを作成できる」サービスを開発しています。
そのサービスのフロントエンドの開発にはReact×Reduxを使っているのですが、 サービス特性のため「Reduxのstateがファットになりがち」であったり「コンポーネント間のデータの依存関係が多くなりがち」といった状態に陥ります。 その結果、アプリの反応速度が遅くなりがちです...
この記事では、上のような状態に陥った「React×Redux」アプリの高速化の一例をサンプルアプリを用いて説明します。
サンプルアプリについて
今回、Reduxのチュートリアルにあるカウンターアプリを改良し、下のような「モンスターの捕獲記録」アプリを作ります。
Version1(コンポーネントをブルブルさせてみる)
まず、下のような構成でアプリを実装してみました。
この構成で問題なのは、MonstersListコンポーネントが全モンスターの情報を取得してしまっている点です。
export const MonstersList = () => { // 全モンスターのリストを取得している const monsters = useAppSelector(selectMonstersList); return ( <div className="monsters"> <div> {monsters.map((monster, key) => ( <MonsterItem key={key} monster={monster} /> ))} </div> <div className="footer"> <Summaries /> </div> </div> ); };
いずれかのCounterコンポーネントからactionが発行されstateの変更が起きるとselectorが再度発火してしまうため、MonsterListコンポーネント全体の再レンダリングが発生してしまいます。
今回のサンプルアプリの場合だと400件のMonsterItemの再レンダリングなので速度低下に繋がりそうです。
※今回、私の開発環境での体感では遅延は全く気にならないレベルでしたが、将来的にモンスター画像を載せたり、個体値情報を載せたり機能を盛っていくとみるみる遅くなると思います
ここで、本当に再レンダリングが起きているのかを確認しておきましょう。 具体的にはMonsterItemコンポーネントのレンダリングのタイミングでコンポーネントをブルブル動かすアニメーションをつけてみます。
※Chrome拡張のReact DevToolの機能で手軽にレンダリングの状態を可視化することができます!個人的に無職やめ太郎さんの記事などがわかりやすいのでそちらもぜひ試してください。
export const MonsterItem = ({ monster }: { monster: MonsterType }) => { // コンポーネントをブルブル動かすアニメーション const controls = useAnimation(); controls.start({ rotate: [0, 10, 0, -10, 0, 5, 0, -5, 0] }); return ( <motion.div className="item" animate={controls}> <div className="item-header"> <div className="name">{monster.name}</div> <div className="attributes">{monster.attributes}</div> </div> <Counter count={monster.count} no={monster.no} /> </motion.div> ); };
どうでしょう、関係のないコンポーネントが軒並みブルブルしているのが確認できると思います。
Version2(構成を見直す)
では、サンプルアプリを改善していきます。 下のようにselectorを「リストサイズの取得」と「単一の情報の取得」との2つに分割し、List/Itemのそれぞれのコンポーネントから呼び分ける構成に変更しました。
結果、画面も変更のあった箇所だけブルブルするようになり静かになりました🌱
と、安堵していたところに下のような要件が追加されたとします
(追加の要件) モンスターのタイプ毎の集計結果に、性別の内訳を表示したい
試しに集計用のselectorの戻り値を数値からオブジェクトに変更してみます。
// 指定されたタイプのモンスターの捕獲数を集計して返却する export const selectSummaryByAttribute = (attribute: MonsterAttribute) => ( state: RootState ) => { const total = state.monsters.list .filter((monster) => monster.attributes.includes(attribute)) .reduce((prev, current) => prev + current.count, 0); // TODO 性別の内訳を返却する return { total, male: 1, female: 1 }; };
すると、集計欄が全体的にブルブルするようになりました...
Version3(Reduxのドキュメントを読む)
Reduxのドキュメントを見ると下のような記述がありました。 selectorの戻り値の型をオブジェクトにしたことで、中身の値が同じでも===での判定がfalseになってしまったようです。
useSelector compares its results using strict === reference comparisons, so the component will re-render any time the selector result is a new reference!
また、createSelectorなどmemo化されたselectorの記述などあり、 やはり困った際はドキュメントを見るのがいいですね!
ここまでのソースは下になります。
さいごに
ここまで読んでくださりありがとうございます!
最後に宣伝になりますが、カミナシでは、下のような職種を幅広く募集しております!
- ソフトウェアエンジニア(フロントエンド、バックエンド、セキュリティ、デザインシステム領域のフロントエンド)
- エンジニアリングマネージャー
一緒に働いていただけるかた、もしよろしければご応募お待ちしております!