React をリアクティブにする: 高性能でメンテナンスしやすい React アプリの追求
2年3月2016日編集: MobservableはMobXにブランド名が変更されました
超高速な React アプリを構築するにはどうすればよいでしょうか。最近、大規模プロジェクトの 40 つで React を使い始めましたが、構造化されたコンポーネント構築方法と、大量の UI 更新を節約できる高速な仮想 DOM のおかげで、React は大いに役立っています。このプロジェクトの優れた点は、なかなか難しい課題がいくつかあることです。ブラウザーで何千ものオブジェクトを描画する必要があり、これらのオブジェクトは互いに高度に結合しています。XNUMX つのオブジェクトの値は任意の数の他のオブジェクトで使用される可能性があるため、小さな変更でも UI の無関係な部分の多くで更新が必要になる場合があります。これらの値はユーザーのドラッグ アンド ドロップ操作によって更新される可能性があるため、UI の応答性を維持するには、すべての更新と再描画を XNUMX ミリ秒未満で実行する必要があります。また、プレーンな React は高速ですが、React だけでは十分ではないことにすぐに気付きました。
そこで私たちは、必要なパフォーマンスを提供しつつ、Reactの原則に従ってコードベースを保守できるソリューションを探し始めました。つまり、 エレガント そこで私たちは関数型リアクティブプログラミングの世界の概念を活用しようとしました。 観測可能なもの観測可能なもののセールスポイントは、すべての計算が、他のどの観測可能なものを使用するかを自動的に検出することです。将来これらの観測可能なものの 1 つが変更されると、計算は自動的に再評価されます。 観測可能なもの EmberやKnockoutなどの他のUIフレームワークでも使用されている概念です。すべてのモデルオブジェクトが 観測可能なもの そしてすべてのReactコンポーネントは オブザーバー モデルが完成すれば、UI の関連部分だけが更新されるようにするために、さらに魔法をかける必要はなくなります。続きを読んで、そのすばらしさをすべて確認してください。最後には偶数も出てきます。
理論的でない(あるいは、好みに応じて、より簡素な)例から始めましょう。小さなショップを表す React アプリを想像してください。いくつかの商品があり、これらの商品のいくつかを入れることができるショッピング カートがあります。次のようになります。

ふぅ、 そこ それは現実の生活の中にあります。
データモデル
まず、データ モデルを定義しましょう。名前と価格を持つ商品があり、合計金額を持つショッピング カートがあります。合計金額は、そのエントリの合計に基づいています。各エントリは商品を参照し、金額を保存し、派生価格を持っています。データ モデル内の関係は、以下のように視覚化されています。開いた丸印は、他のデータが変更された場合に更新する必要がある派生データを表し、UI での表現も同様に更新する必要があります。したがって、この単純なモデルでも、大量のデータが流通しており、変更があった場合は多くの UI 更新が必要になります。

要件のリストをまとめてみましょう:
- 商品の価格が変更された場合、関連するすべてのショッピング カート エントリの価格が再評価される必要があります。
- カートの合計コストも同様です。
- カート内の商品の数量が変わった場合は、合計コストを更新する必要があります。
- 記事の名前が変更された場合は、そのビューを更新する必要があります
- 記事の名前が変更された場合は、関連するカートエントリの表示が更新されます。
- 新しい商品がカートに追加された場合。
- などなど。
おそらく、ここまでで UI の問題の要点は明らかでしょう。プログラマーとしては、あらゆる種類の更新を処理する定型コードを記述したくありませんが、データが変更されるたびにアプリケーションが常に再レンダリングされると、ユーザーは不便なほど長く待たされる可能性があります。
それでは、この問題を完全に解決して、データ モデルを記述してみましょう。
まあ、そんなに難しくなかったでしょう?上記のコンストラクタ関数は、 モブエックス ライブラリは、オブザーバブルコンセプトのスタンドアロン実装を提供します(Reactと同様に、他のJavaScriptベースのライブラリと簡単に組み合わせることができます)。 props 関数は、指定されたキーと値の型に基づいて、ターゲットオブジェクトに新しい監視可能なプロパティを作成します。すべてのプロパティが監視可能なため、上記の関数は依存関係の一部が変更されたときに自動的に(そしてのみ)更新されます。これにより、たとえば、いくつかの要件がすぐに満たされます。 total 新しいエントリが追加されたとき、商品の価格が変更されたときなどに、カートの内容は自動的に更新されます。
ユーザーインターフェース
百聞は一見にしかずなので、このモデルを中心にユーザー インターフェイスを構築しましょう。初期データをレンダリングする React コンポーネントをいくつか作成します。次の JSX スニペットは、ショッピング カートのビューを表示し、カート内のすべてのエントリをレンダリングして、カートの合計価格を表示します。ご想像のとおり、記事のビューなど、アプリ内の他のコンポーネントも非常に似ています。
かなり簡単ですよね?CartViewコンポーネントはカートを受け取り、CartEntryViewを使用して合計と個々のアイテムをレンダリングし、関連する記事の名前と希望する記事の数を出力します。Reactのベストプラクティスによると、リストされた各アイテムは一意に識別可能である必要があるため、各エントリに任意の不変IDを割り当てます。削除ボタンは、この数を1減らし、ゼロになるとエントリ全体がカートから削除されます。 removeArticle 関数では、UI を更新する必要があることを示しました。
次の大きなステップは、たとえばエントリが削除されたときに、これらのコンポーネントをデータ モデルに合わせて最新の状態に保つように強制することです。レンダリング コードから簡単に判断できるように、データ遷移は多数考えられます。記事の数、カートの合計コスト、記事の名前、さらにはエントリと記事間の参照が変わることもあります。これらすべての変更をどのように監視するのでしょうか。
まあ、それはかなり簡単です。 mobxReact.observer mobx-react 各コンポーネントにパッケージ化すれば、他のすべての要件を満たすのに十分です。
え、それだけ?はい、デモと上記のソースをチェックしてください。 JSフィドルそれで何が起こったのでしょうか? observer この関数は 6 つのことを行いました。まず、コンポーネントのレンダリング関数を監視可能な関数に変換しました。次に、コンポーネント自体がその関数のオブザーバーとして登録されたため、レンダリングが古くなるたびに再レンダリングが強制されます。したがって、この関数 (および ESXNUMX を使用している場合はデコレータ) により、監視可能なデータが変更されるたびに、UI の関連部分のみが更新されます。サンプル アプリでいろいろと試してみて、その間、ログ パネルに注意して、実際のアクションと実際のデータに基づいて UI がどのように更新されるかを確認してください。
- ショッピングカートにない商品の名前を変更しようとする
- 商品をカートに追加し、名前を変更する
- 商品をカートに追加し、価格を更新する
- カートから削除し、価格を再度更新します
- …など。各アクションで、最小限のコンポーネントが再レンダリングされることがわかります。
最後に、各コンポーネントは独自の依存関係を追跡するため、通常、コンポーネントの子を明示的に再レンダリングする必要はありません。たとえば、ショッピングカートの合計が再レンダリングされる場合、エントリも再レンダリングする必要はありません。Reactの独自の ピュアレンダーミックスイン それが起こらないようにします。
番号
それで、私たちは何を達成したのでしょうか?比較のために、 こちら まったく同じアプリですが、Observable がなく、単純な「すべてを再レンダリングする」アプローチを採用しているものもあります。記事が数個しかない場合は違いに気付かないでしょうが、記事の数が増えると、パフォーマンスの違いが非常に顕著になります。


大量のデータとコンポーネントを作成すると、オブザーバブルの有無にかかわらず、動作は非常に似ています。しかし、データが変更されると、オブザーバブルが真価を発揮し始めます。10 項目のコレクション内の 10,000 件の記事を更新すると、約 2.5 倍高速になります。250 秒が 10 ミリ秒に短縮されました。これが、遅延のあるエクスペリエンスと遅延のないエクスペリエンスの違いです。この違いはどこから来るのでしょうか。まず、オブザーバブルなしで「10000 件の記事のリスト内の XNUMX 件の記事を更新する」シナリオで更新を実行した後の React レンダリング レポートを見てみましょう。

ご覧のとおり、2,145 万個の ArticleView と CartEntryView がすべて再レンダリングされています。ただし、React によると、レンダリング時間の合計 2,433 ミリ秒のうち XNUMX ミリ秒が無駄になっています。無駄になったというのは、実際の DOM の更新につながらなかったレンダリング関数の実行に費やされた時間です。これは、コンポーネントが多数ある場合、すべてを単純に再レンダリングすることは CPU 時間の大きな無駄であることを強く示唆しています。比較のために、Observable を使用した場合の同じシナリオのレポートを以下に示します。

これは大きな違いです。20,006 個のコンポーネントを再レンダリングする代わりに、31 個のコンポーネントのみが再レンダリングされます。さらに重要なのは、無駄が報告されないことです。つまり、再レンダリングされたすべてのコンポーネントが実際に DOM 内の何かを変更したということです。これはまさに、Observable を使用して実現しようとしていたことです。
レポートから、残りのレンダリング時間のほとんど、合計 243 ミリ秒のうち 267 ミリ秒が、カートの合計コストを更新するために再レンダリングされる CartView のレンダリングに費やされていることが明らかになります。ただし、CartView の再レンダリングは、CartEntryView への引数のいずれかが変更されたかどうかを確認するために、すべての 60 万エントリを再確認することを意味します。したがって、CartView の合計を独自のコンポーネントである CartTotalView に配置するだけで、合計コストが変更された場合にのみ CartView のレンダリング全体をスキップできます。これにより、レンダリング時間はさらに約 40 ミリ秒に短縮されます (上のグラフの「最適化」シリーズを参照)。これは、バニラ React アプリで同じ更新を行った場合の約 XNUMX 倍の速さです。
結論
Observables を使用することで、すべてのコンポーネントを単純に再レンダリングする同じアプリよりも桁違いに高速なアプリケーションを構築できました。そして、(プログラマーの皆さんにとって) 重要なのは、コードの保守性を損なうことなくこれを実現できたことです。上にリンクされている 2 つの JSFiddle のソース コードを見てください。2 つのリストは非常に似ており、どちらも同じように使いやすいです。
他の技術でも同じことが実現できたでしょうか? おそらくできるでしょう。たとえば、ImmutableJS は、変更されたデータを受け取ったコンポーネントのみを更新することで、React のレンダリングを非常に高速化します。ただし、データ モデルで大幅な譲歩をする必要があります。結局のところ、私見では、可変クラスは不変クラスよりも扱いやすいです。さらに、不変データ構造は、計算された値を最新の状態に保つのに役立ちません。したがって、不変データを使用すると、記事の名前を変更すると、ArticleView が非常に高速に再レンダリングされますが、同じ記事を参照する既存の CartEntryViews は無効になりません。
React アプリを最適化するために適用できるもう 1 つの手法は、データ内の可能性のある各変更に対してイベントを作成し、適切なタイミングで適切なコンポーネントでそれらのイベントのリスナーを登録 (または登録解除) することです。ただし、これにより大量の定型コードが生成され、メンテナンス時にエラーが発生しやすくなります。また、私はそのようなことをするのが面倒すぎると思います。
ちなみに、プロジェクト内での関心の分離を明確に保つために、モデル データの更新に関する抽象化としてコントローラーまたはアクション ディスパッチャーを使用することを強くお勧めします。
結論として、大規模なプロジェクトでは、React と Observables を組み合わせると非常にうまく機能し、パフォーマンスの問題が発生することなく、まだ考えも及ばないコーナーケースでデータ変更によって UI が正しく更新されることが時々ありました。そのため、私としては、いつ、どのように UI をできるだけ早く更新するかを考えるという大変な作業を React と Observables に任せて、コーディングの興味深い部分に集中することにします :)。
この投稿について議論する ハッカーニュース.