効率的なReactコンポーネント:Reactパフォーマンスを最適化するためのガイド
公開: 2022-03-11Reactは、その導入以来、フロントエンド開発者がWebアプリを構築する方法を変えてきました。 仮想DOMを使用すると、ReactはUIの更新をこれまでになく効率的にし、Webアプリをよりスッキリさせます。 しかし、なぜ適度なサイズのReact Webアプリは、依然としてパフォーマンスが低下する傾向があるのでしょうか。
手がかりは、Reactをどのように使用しているかにあります。
Reactのような最新のフロントエンドライブラリは、魔法のようにアプリを高速化するわけではありません。 開発者は、Reactがどのように機能し、コンポーネントがコンポーネントのライフサイクルのさまざまなフェーズでどのように機能するかを理解する必要があります。
Reactを使用すると、コンポーネントがレンダリングされる方法とタイミングを測定および最適化することで、パフォーマンスを大幅に向上させることができます。 そして、Reactはこれを簡単にするために必要なツールと機能だけを提供します。
このReactチュートリアルでは、Reactコンポーネントのパフォーマンスを測定し、それらを最適化して、はるかにパフォーマンスの高いReactWebアプリを構築する方法を学習します。 また、いくつかのJavaScriptのベストプラクティスが、ReactWebアプリがはるかに流暢なユーザーエクスペリエンスを提供するのにどのように役立つかについても学びます。
Reactはどのように機能しますか?
最適化手法に飛び込む前に、Reactがどのように機能するかをよりよく理解する必要があります。
React開発の中核には、シンプルで明白なJSX構文と、仮想DOMを構築および比較するReactの機能があります。 そのリリース以来、Reactは他の多くのフロントエンドライブラリに影響を与えてきました。 Vue.jsなどのライブラリも、仮想DOMの概念に依存しています。
Reactの仕組みは次のとおりです。
各Reactアプリケーションはルートコンポーネントで始まり、ツリー構成の多くのコンポーネントで構成されています。 Reactのコンポーネントは、受け取ったデータ(小道具と状態)に基づいてUIをレンダリングする「関数」です。
これをF
として象徴することができます。
UI = F(data)
ユーザーはUIを操作し、データを変更します。 インタラクションにボタンのクリック、画像のタップ、リストアイテムのドラッグ、AJAXリクエストのAPIの呼び出しなどが含まれるかどうかにかかわらず、これらのインタラクションはすべてデータを変更するだけです。 UIが直接変更されることはありません。
ここで、データとは、データベースに保存したものだけでなく、Webアプリケーションの状態を定義するすべてのものです。 フロントエンドの状態のビット(たとえば、現在選択されているタブやチェックボックスが現在チェックされているかどうか)もこのデータの一部です。
このデータに変更がある場合は常に、Reactはコンポーネント関数を使用してUIを再レンダリングしますが、仮想的には次のようになります。
UI1 = F(data1) UI2 = F(data2)
Reactは、仮想DOMの2つのバージョンに比較アルゴリズムを適用することにより、現在のUIと新しいUIの違いを計算します。
Changes = Diff(UI1, UI2)
次に、Reactは、UIの変更のみをブラウザーの実際のUIに適用します。
コンポーネントに関連付けられたデータが変更されると、Reactは実際のDOM更新が必要かどうかを判断します。 これにより、Reactは、DOMノードの作成や、必要以上に既存のノードへのアクセスなど、ブラウザーでの潜在的にコストのかかるDOM操作操作を回避できます。
この繰り返しのコンポーネントの差分とレンダリングは、ReactアプリのReactパフォーマンスの問題の主な原因の1つになる可能性があります。 差分アルゴリズムが効果的に調整できないReactアプリを構築すると、アプリ全体が繰り返しレンダリングされるため、イライラするほど遅いエクスペリエンスが発生する可能性があります。
最適化を開始する場所は?
しかし、私たちが最適化するのは正確には何ですか?
ご覧のとおり、最初のレンダリングプロセス中に、Reactは次のようなDOMツリーを構築します。
データの変更の一部が与えられた場合、Reactに実行させたいのは、変更によって直接影響を受けるコンポーネントのみを再レンダリングすることです(残りのコンポーネントの差分プロセスもスキップする可能性があります)。
ただし、Reactが実行するのは次のとおりです。
上の画像では、すべての黄色のノードがレンダリングおよび差分されているため、時間/計算リソースが無駄になっています。 ここで主に最適化の取り組みを行います。必要な場合にのみrender-diffを実行するように各コンポーネントを構成すると、これらの無駄なCPUサイクルを再利用できます。
Reactライブラリの開発者はこれを考慮に入れて、まさにそれを行うためのフックを提供しました。それは、コンポーネントのレンダリングをスキップしてもよいときにReactに通知できる関数です。
最初に測定する
Rob Pikeが、プログラミングのルールの1つとして、それをかなりエレガントに表現しているように、次のようになります。
測定。 測定するまで速度を調整しないでください。それでも、コードの一部が残りの部分を圧倒しない限り、調整しないでください。
アプリの速度が低下していると思われるコードの最適化を開始しないでください。 代わりに、Reactパフォーマンス測定ツールが道を案内します。
Reactには、このための強力なツールがあります。 react-addons-perf
ライブラリを使用すると、アプリの全体的なパフォーマンスの概要を取得できます。
使用法は非常に簡単です:
Import Perf from 'react-addons-perf' Perf.start(); // use the app Perf.stop(); Perf.printWasted();
これにより、コンポーネントがレンダリングで浪費された時間の表が印刷されます。
ライブラリには、無駄な時間のさまざまな側面を個別に印刷できる他の関数が用意されています(たとえば、 printInclusive()
またはprintExclusive()
関数を使用)。また、DOM操作操作を印刷することもできます( printOperations()
関数を使用)。
ベンチマークをさらに一歩進める
あなたが視覚的な人なら、 react-perf-tool
はまさにあなたが必要とするものです。
react-perf-tool
は、 react-addons-perf
ライブラリに基づいています。 これにより、Reactアプリのパフォーマンスをより視覚的にデバッグできます。 基盤となるライブラリを使用して測定値を取得し、それらをグラフとして視覚化します。
多くの場合、これはボトルネックを見つけるためのはるかに便利な方法です。 アプリケーションにコンポーネントとして追加することで、簡単に使用できます。
Reactはコンポーネントを更新する必要がありますか?
デフォルトでは、Reactが実行され、仮想DOMがレンダリングされ、ツリー内のすべてのコンポーネントの違いが比較され、その小道具や状態が変化します。 しかし、それは明らかに合理的ではありません。
アプリが大きくなるにつれて、すべてのアクションで仮想DOM全体を再レンダリングして比較しようとすると、最終的に速度が低下します。
Reactは、開発者がコンポーネントの再レンダリングが必要かどうかを示す簡単な方法を提供します。 ここで、 shouldComponentUpdate
メソッドが役立ちます。
function shouldComponentUpdate(nextProps, nextState) { return true; }
この関数がいずれかのコンポーネントに対してtrueを返すと、render-diffプロセスをトリガーできます。
これにより、render-diffプロセスを簡単に制御できます。 コンポーネントが再レンダリングされないようにする必要がある場合は、関数からfalse
を返すだけです。 関数内で、現在と次の小道具と状態のセットを比較して、再レンダリングが必要かどうかを判断できます。
function shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id; }
React.PureComponentの使用
この最適化手法を少し簡単にして自動化するために、Reactは「純粋な」コンポーネントとして知られているものを提供します。 React.PureComponent
は、浅い小道具と状態の比較でshouldComponentUpdate()
関数を実装するReact.Component
とまったく同じです。

React.PureComponent
は、多かれ少なかれこれと同等です。
class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState); } … }
浅い比較しか実行しないため、次の場合にのみ役立つことがあります。
- 小道具または状態には、プリミティブデータが含まれています。
- 小道具と状態には複雑なデータがありますが、
forceUpdate()
を呼び出してコンポーネントを更新するタイミングはわかっています。
データを不変にする
React.PureComponent
を使用できても、複雑な小道具や状態が自動的に変更されたことを効率的に確認できる場合はどうでしょうか。 これは、不変のデータ構造が生活を楽にする場所です。
不変のデータ構造を使用する背後にある考え方は単純です。 複雑なデータを含むオブジェクトが変更されるたびに、そのオブジェクトに変更を加える代わりに、変更を加えてそのオブジェクトのコピーを作成します。 これにより、2つのオブジェクトの参照を比較するのと同じくらい簡単にデータの変更を検出できます。
Object.assign
または_.extend
(Underscore.jsまたはLodashから)を使用できます。
const newValue2 = Object.assign({}, oldValue); const newValue2 = _.extend({}, oldValue);
さらに良いことに、不変のデータ構造を提供するライブラリを使用できます。
var map1 = Immutable.Map({a:1, b:2, c:3}); var map2 = map1.set('b', 2); assert(map1.equals(map2) === true); var map3 = map1.set('b', 50); assert(map1.equals(map3) === false);
ここで、 Immutable.Map
はライブラリImmutable.jsによって提供されます。
マップがそのメソッドset
で更新されるたびに、set操作が基になる値を変更した場合にのみ新しいマップが返されます。 それ以外の場合は、同じマップが返されます。
不変のデータ構造の使用について詳しくは、こちらをご覧ください。
より多くのReactアプリ最適化手法
プロダクションビルドの使用
Reactアプリを開発するとき、本当に便利な警告とエラーメッセージが表示されます。 これらにより、開発中のバグや問題の特定が至福になります。 ただし、パフォーマンスが犠牲になります。
Reactのソースコードを調べると、多くのif (process.env.NODE_ENV != 'production')
チェックが表示されます。 Reactが開発環境で実行しているこれらのコードのチャンクは、エンドユーザーが必要とするものではありません。 実稼働環境では、この不要なコードをすべて破棄できます。
create-react-app
を使用してプロジェクトをブートストラップした場合は、 npm run build
を実行するだけで、この余分なコードなしで本番ビルドを生成できます。 Webpackを直接使用している場合は、 webpack -p
( webpack --optimize-minimize --define process.env.NODE_ENV="'production'"
と同等)を実行できます。
早期のバインディング機能
レンダリング関数内のコンポーネントのコンテキストにバインドされた関数を確認することは非常に一般的です。 これは、これらの関数を使用して子コンポーネントのイベントを処理する場合によくあります。
// Creates a new `handleUpload` function during each render() <TopBar onUpload={this.handleUpload.bind(this)} /> // ...as do inlined arrow functions <TopBar onUpload={files => this.handleUpload(files)} />
これにより、 render()
関数はすべてのレンダリングで新しい関数を作成します。 同じことを行うためのはるかに良い方法は次のとおりです。
class App extends React.Component { constructor(props) { super(props); this.handleUpload = this.handleUpload.bind(this); } render() { … <TopBar onUpload={this.handleUpload} /> … } }
複数のチャンクファイルを使用する
単一ページのReactWebアプリの場合、多くの場合、すべてのフロントエンドJavaScriptコードを単一の縮小ファイルにバンドルすることになります。 これは、小規模から中規模のWebアプリで正常に機能します。 ただし、アプリが成長し始めると、このバンドルされたJavaScriptファイルをブラウザー自体に配信すること自体が時間のかかるプロセスになる可能性があります。
Webpackを使用してReactアプリをビルドしている場合は、そのコード分割機能を利用して、ビルドしたアプリコードを複数の「チャンク」に分割し、必要に応じてブラウザーに配信できます。
分割には、リソース分割とオンデマンドコード分割の2種類があります。
リソース分割では、リソースコンテンツを複数のファイルに分割します。 たとえば、CommonsChunkPluginを使用すると、一般的なコード(すべての外部ライブラリなど)を独自の「チャンク」ファイルに抽出できます。 ExtractTextWebpackPluginを使用すると、すべてのCSSコードを個別のCSSファイルに抽出できます。
この種の分割は、2つの方法で役立ちます。 これは、ブラウザが頻繁に変更されないリソースをキャッシュするのに役立ちます。 また、ブラウザが並列ダウンロードを利用して、読み込み時間を短縮できる可能性もあります。
Webpackのより注目すべき機能は、オンデマンドのコード分割です。 これを使用して、オンデマンドでロードできるチャンクにコードを分割できます。 これにより、初期ダウンロードを小さく保つことができ、アプリの読み込みにかかる時間を短縮できます。 ブラウザは、アプリケーションで必要なときに、オンデマンドで他のコードのチャンクをダウンロードできます。
Webpackコード分割について詳しくは、こちらをご覧ください。
WebサーバーでGzipを有効にする
ReactアプリのバンドルJSファイルは一般的に非常に大きいため、Webページの読み込みを高速化するために、Webサーバー(Apache、Nginxなど)でGzipを有効にすることができます。
最新のブラウザはすべて、HTTPリクエストのGzip圧縮をサポートし、自動的にネゴシエートします。 Gzip圧縮を有効にすると、転送される応答のサイズを最大90%削減できます。これにより、リソースのダウンロードにかかる時間が大幅に短縮され、クライアントのデータ使用量が削減され、ページの最初のレンダリングまでの時間が短縮されます。
圧縮を有効にする方法については、Webサーバーのドキュメントを確認してください。
- Apache: mod_deflateを使用します
- Nginx: ngx_http_gzip_moduleを使用します
Eslint-plugin-reactを使用する
ほとんどすべてのJavaScriptプロジェクトにESLintを使用する必要があります。 Reactも例外ではありません。
eslint-plugin-react
を使用すると、Reactプログラミングの多くのルールに適応する必要があります。これにより、長期的にはコードにメリットがあり、コードの記述が不十分なために発生する多くの一般的な問題や問題を回避できます。
ReactWebアプリを再び高速化
Reactを最大限に活用するには、そのツールとテクニックを活用する必要があります。 React Webアプリのパフォーマンスは、そのコンポーネントのシンプルさにあります。 render-diffアルゴリズムを圧倒すると、アプリのパフォーマンスが低下し、イライラする可能性があります。
アプリを最適化する前に、Reactコンポーネントがどのように機能し、ブラウザーでどのようにレンダリングされるかを理解する必要があります。 Reactライフサイクルメソッドは、コンポーネントが不必要に再レンダリングされるのを防ぐ方法を提供します。 これらのボトルネックを解消すると、ユーザーにふさわしいアプリのパフォーマンスが得られます。
React Webアプリを最適化する方法は他にもありますが、必要な場合にのみ更新するようにコンポーネントを微調整すると、最高のパフォーマンスが向上します。
React Webアプリのパフォーマンスをどのように測定および最適化しますか? 以下のコメントでそれを共有してください。