React slow、React Fast:実際のReactアプリケーションの最適化

みなさんこんにちは! 記事React is Slow、React is Fast:React Appsの最適化: FrançoisZaninottoの記事の翻訳を共有したいと思います。 これが誰かに役立つことを願っています。







要約:







  1. 反応性能測定
  2. なぜ更新されたのですか?
  3. コンポーネントの最適化
  4. shouldComponentUpdate
  5. 再構成
  6. Redux
  7. 再選択
  8. JSXのオブジェクトリテラルに注意してください
  9. おわりに


反応が遅くなる場合があります。 中規模のReactアプリケーションは遅くなる可能性があると言いたいです。 ただし、その代替品を探す前に、AngularまたはEmberの平均的なアプリケーションも低速になる可能性があることを知っておく必要があります。







良いニュースは、パフォーマンスを本当に気にするなら、Reactアプリを本当に高速にするのはとても簡単だということです。 これについては、記事の後半で詳しく説明します。







反応性能測定



「遅い」とはどういう意味ですか? 例を挙げましょう。







material-uiReduxを使用して、任意のAPIの管理パネルGUIを提供するadmin -on-restと呼ばれる単一のオープンソースプロジェクトに取り組んでいます。 このアプリケーションには、テーブル内のレコードのリストを表示するページがあります。 ユーザーが並べ替え順序を変更したり、次のページに移動したり、出力をフィルター処理したりすると、インターフェイスの応答が期待どおりになりません。







5回スローダウンした次のアニメーションスクリーンキャストは、更新の様子を示しています。







更新の様子を示す5回のアニメーションスクリーンキャスト



何が起こっているのかを理解するために、URLの最後に?react_perf



を追加します。 これにより、 コンポーネントプロファイリング機能がアクティブになります 。これは、Reactバージョン15.4から利用可能です。 まず、データテーブルが読み込まれるのを待ちます。 次に、Chromeの開発者ツールで[タイムライン]タブを開き、[記録]ボタンをクリックして、ページのテーブルタイトルをクリックして並べ替え順序を更新します。







データを更新した後、もう一度記録ボタンをクリックして停止します。 Chromeでは、「ユーザータイミング」ラベルの下に黄色のグラフが表示されます。







Chromeはユーザータイミングの下に黄色のグラフを表示します



このチャートを見たことがないなら、恐ろしいかもしれませんが、実際には非常に使いやすいです。 このグラフは、各コンポーネントの実行時間を示しています。 内部のReactコンポーネントの時間は表示されないため(まだ最適化できません)、独自のコードの最適化に集中できます。







タイムラインには、アプリケーションの操作を記録する段階が表示され、テーブルヘッダーをクリックした瞬間を概算できます。







タイムラインには、アプリケーションの操作を記録するための手順が表示されます



私のアプリケーションは、ソートボタンをクリックした直後、RESTを介してデータを受信する前に <List>



コンポーネントを再描画するようです。 500ミリ秒以上かかります。 アプリケーションは、テーブルヘッダーの並べ替えアイコンを更新し、データの読み込みを示す灰色の画面を表示するだけです。







つまり、アプリケーションはクリックに対する応答を視覚的に表示するのに500ミリ秒かかります。 0.5秒は重要な指標です。UIの専門家は、 ユーザーがアプリケーションの反応は100ミリ秒未満の場合にのみ瞬間的であると考えると言います 。 100 msを超えるアプリケーション応答は、私が「遅い」と呼んでいるものです。







なぜ更新されたのですか?



上のグラフでは、多くの小さな穴が見られます。 多くのコンポーネントが再描画されたことを意味するため、これは悪い兆候です。 グラフは、 <Datagrid>



更新に最も時間がかかることを示しています。 アプリケーションが新しいデータを受信する前にデータでテーブル全体を更新したのはなぜですか? 正しくしましょう。







再描画の理由を理解しようとすると、多くの場合、 console.log()



render()



関数に追加する必要がありrender()



。 機能コンポーネントの場合、次の高次コンポーネント(HOC)を使用できます。







 // src/log.js const log = BaseComponent => props => { console.log(`Rendering ${BaseComponent.name}`); return <BaseComponent {…props} />; } export default log; // src/MyComponent.js import log from './log'; export default log(MyComponent);
      
      





ヒント: Reactの有効性のためのもう1つのツールである、 更新した理由も注目に値します このnpmパッケージにより、コンポーネントが同じ小道具で再描画されるたびに、Reactはコンソールに警告を出力します。 警告:コンソールの出力は非常に詳細であり、機能コンポーネントでは機能しません。


この例では、ユーザーが列見出しをクリックすると、アプリケーションは状態を変更するアクションを実行します。リストのソート順( currentSort



)が更新されます。 この状態の変更により、 <List>



ページの再描画がトリガーされ、これにより<Datagrid>



コンポーネント全体が再描画されます。 ユーザーのアクションへの応答として、テーブルのタイトルに並べ替えアイコンの変更をすぐに表示させます。







通常、Reactは1つの遅いコンポーネント(1つの大きな穴としてチャートに表示されます)のためではなく、遅くなります。 ほとんどの場合、Reactは多くのコンポーネントの無駄な再描画のために遅くなります。







ReactのVirtualDOMが非常に高速であることを読んだかもしれません。 これは事実ですが、中規模のアプリケーションでは、完全な再描画に数百のコンポーネントを簡単に含めることができます。 最速のVirtualDOMテンプレートエンジンでさえ、16ミリ秒未満でこれを行うことはできません。







コンポーネントの最適化



<Datagrid>



コンポーネントのrender()



メソッドは次のとおりです。







 // Datagrid.js render() { const { resource, children, ids, data, currentSort } = this.props; return ( <table> <thead> <tr> {Children.map(children, (field, index) => <DatagridHeaderCell key={index} field={field} currentSort={currentSort} updateSort={this.updateSort} /> )} </tr> </thead> <tbody> {ids.map(id => ( <tr key={id}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> </table> ); }
      
      





これは、表形式データの非常に単純な実装のようですが、非常に非効率的です。 各<DatagridCell>



により、少なくとも2つまたは3つのコンポーネントが描画されます。 記事の冒頭にあるインターフェースのアニメーションスクリーンキャストを見るとわかるように、リストには7列と11行が含まれています。つまり、7 * 11 * 3 = 231コンポーネントが再描画されます。 currentSort



のみがcurrentSort



れるcurrentSort



ため、これはすべて時間の無駄です。 Reactは実際のDOMを更新しないという事実にもかかわらず(VirtualDOMが変更されていないと仮定)、すべてのコンポーネントを処理するのに約500ミリ秒かかります。







テーブルの本体の無駄な再描画を避けるために、最初に* 抽出 *しなければなりません:







 // Datagrid.js render() { const { resource, children, ids, data, currentSort } = this.props; return ( <table> <thead> <tr> {React.Children.map(children, (field, index) => <DatagridHeaderCell key={index} field={field} currentSort={currentSort} updateSort={this.updateSort} /> )} </tr> </thead> <DatagridBody resource={resource} ids={ids} data={data}> {children} </DatagridBody> </table> ); ); }
      
      





テーブルの本体からロジックを抽出して、新しい<DatagridBody>



コンポーネントを作成しました。







 // DatagridBody.js import React, { Children } from 'react'; const DatagridBody = ({ resource, ids, data, children }) => ( <tbody> {ids.map(id => ( <tr key={id}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> ); export default DatagridBody;
      
      





それ自体では、テーブル本体を抽出してもパフォーマンスに影響はありませんが、最適化の機会が開かれます。 大規模な汎用コンポーネントは最適化が困難です。 1つのことだけを担当する小さなコンポーネントを使用すると、扱いやすくなります。







shouldComponentUpdate



Reactのドキュメントには、 shouldComponentUpdate()



を使用して無駄な再描画を回避する方法が非常に明確に説明されています。 デフォルトでは、Reactは常にコンポーネントをVirtualDOMにマップします。 つまり、開発者としてのあなたの仕事は、コンポーネントの小道具が変更されたかどうかを確認し、変更されていない場合は再描画をスキップすることです。







<DatagridBody>



コンポーネントの場合、小道具が変更されるまで再描画しないでください。







したがって、コンポーネントは次のようになります。







 import React, { Children, Component } from 'react'; class DatagridBody extends Component { shouldComponentUpdate(nextProps) { return (nextProps.ids !== this.props.ids || nextProps.data !== this.props.data); } render() { const { resource, ids, data, children } = this.props; return ( <tbody> {ids.map(id => ( <tr key={id}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> ); } } export default DatagridBody;
      
      





ヒント: shouldComponentUpdate()を手動で記述する代わりに、このクラスをComponentではなくPureComponentから継承できます PureComponentは、厳密な比較( === )を使用してすべての小道具を比較し、小道具が変更された場合にのみ再描画します。 しかし、このコンテキストではリソースを変更できないことを知っているので、それらを比較する必要はありません。


この最適化により、テーブルヘッダーをクリックした後に<Datagrid>



再描画すると、そのコンテンツとすべての231コンポーネントがスキップされます。 これにより、更新時間が500ミリ秒から60ミリ秒に短縮されました。 これは、400ミリ秒以上の純パフォーマンスの向上です!







最適化後



ヒント:チャートの幅にだまされないでください。前のチャートよりもさらに近似されています。 これは間違いなく優れています!


shouldComponentUpdate



メソッドは、チャートの多くの穴を削除し、合計レンダリング時間を短縮しました。 同じメソッドを使用して、さらに再描画を避けることができます(たとえば、サイドバー、アクションボタン、変更されていないテーブルヘッダー、ページネーションを再描画しないでください)。 これらすべてのことで約1時間大騒ぎした後、列見出しをクリックすると、ページ全体がわずか100ミリ秒で描画されます。 これは十分に高速です-最適化するものがまだある場合でも。







shouldComponentUpdate



メソッドの追加は面倒に思えるかもしれませんが、 shouldComponentUpdate



する場合は、ほとんどのコンポーネントに含める必要があります。







ただし、できる限り貼り付けないでください。かなり単純なコンポーネントでshouldComponentUpdate



を実行すると、レンダリングが遅くなることがあります。 アプリケーションのライフサイクルの早い段階でこれを行わないでください。 このメソッドは、コンポーネントのパフォーマンスの問題を特定できる場合に、アプリケーションが成長するときにのみ追加してください。







再構成



<DatagridBody>



に対する以前の変更には特に満足していませんshouldComponentUpdate



ため、シンプルで機能的なコンポーネントをクラスに変換する必要がありました。 これにより、多くのコード行が追加されます。各行には独自の価格があります(作成、デバッグ、サポートの形式)。







幸いなことに、 recomposeのおかげでshouldComponentUpdate



ロジックを高次コンポーネント(HOC)に実装できます 。 これは、Reactの機能的なツールキットであり、たとえば、HOC pure()



関数を提供します。







 // DatagridBody.js import React, { Children } from 'react'; import pure from 'recompose/pure'; const DatagridBody = ({ resource, ids, data, children }) => ( <tbody> {ids.map(id => ( <tr key={id}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> ); export default pure(DatagridBody);
      
      





このコードと最初の実装の唯一の違いは最後の行にあります: pure(DatagridBody)



代わりにpure(DatagridBody)



をエクスポートしDatagridBody



pure



PureComponent



に似ていPureComponent



が、追加の定型文はありません。







私はさらに具体的になり、 pure()



代わりにshouldUpdate()



を使用して変更できることを確信している小道具にのみ焦点を当てることができます:







 // DatagridBody.js import React, { Children } from 'react'; import shouldUpdate from 'recompose/shouldUpdate'; const DatagridBody = ({ resource, ids, data, children }) => ( ... ); const checkPropsChange = (props, nextProps) => (nextProps.ids !== props.ids || nextProps.data !== props.data); export default shouldUpdate(checkPropsChange)(DatagridBody);
      
      





checkPropsChange



は純粋な関数であり、単体テスト用にエクスポートすることもできます。







再構成ライブラリは、 onlyUpdateForKeys()



など、より効率的なHOCを提供し、 onlyUpdateForKeys()



で行ったのと同じチェックを実行しcheckPropsChange









 // DatagridBody.js import React, { Children } from 'react'; import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; const DatagridBody = ({ resource, ids, data, children }) => ( ... ); export default onlyUpdateForKeys(['ids', 'data'])(DatagridBody);
      
      





recompose



を強くお勧めしrecompose



。 パフォーマンスの最適化に加えて、データサンプリングロジックの抽出、HOCのコンパイル、および機能的でテスト可能なスタイルでの小道具の操作を支援します。







Redux



Reduxこれもお勧めします )を使用してアプリケーションの状態を制御する場合、それに接続されているコンポーネントは既にクリーンです。 他のHOCは必要ありません。







1つのプロパティのみが変更された場合、接続されたコンポーネントとそのすべての子孫が再描画されることを覚えておいてください。 したがって、ページコンポーネントにReduxを使用している場合は、ツリーの下のコンポーネントにpure()



またはshouldUpdate()



を使用する必要があります。







しかし、Reduxは小道具に対して厳密な比較を使用していることも覚えておいてください。 Reduxは状態をpropsコンポーネントにバインドするため、オブジェクトの状態を変更すると、Reduxは単にそれを無視します。 このため、レデューサーでは不変性を使用する必要があります。







たとえば、admin-on-restでテーブルの見出しをクリックすると、 SET_SORT



アクションがディスパッチされます。 このアクションをリッスンするReducerは、状態にあるオブジェクトを置き換え、 更新するべきではありません:







 // listReducer.js export const SORT_ASC = 'ASC'; export const SORT_DESC = 'DESC'; const initialState = { sort: 'id', order: SORT_DESC, page: 1, perPage: 25, filter: {}, }; export default (previousState = initialState, { type, payload }) => { switch (type) { case SET_SORT: if (payload === previousState.sort) { //    return { ...previousState, order: oppositeOrder(previousState.order), page: 1, }; } //   sort return { ...previousState, sort: payload, order: SORT_ASC, page: 1, }; // ... default: return previousState; } };
      
      





このレデューサーのコードに従って、Reduxはトリプル比較を使用して変更の状態をチェックするときに、状態オブジェクトが変更されたことを検出し、データテーブルを再描画します。 ただし、状態を変更した場合、Reduxはこの変更をスキップし、それに応じて何も再描画しません。







 //       export default (previousState = initialState, { type, payload }) => { switch (type) { case SET_SORT: if (payload === previousState.sort) { //     previousState.order= oppositeOrder(previousState.order); return previousState; } //      previousState.sort = payload; previousState.order = SORT_ASC; previousState.page = 1; return previousState; // ... default: return previousState; } };
      
      





不変のレデューサーを作成するために、一部の開発者はFacebookのimmutable.jsライブラリを使用しています。 しかし、ES6の再構築により、コンポーネントプロパティを選択的に置き換えることが容易になったため、このライブラリは必要ないと思います。 さらに、重い(60 kB)ので、プロジェクトに応じて追加する前によく考えてください。







再選択



Reduxに接続されたコンポーネントの不必要なレンダリングを防ぐために、 mapStateToProps



関数が呼び出されるたびに新しいオブジェクトを返さないことも確認する必要があります。







たとえば、admin-on-restの<List>



コンポーネントを取り上げます。 次のコードを使用して、状態から現在のリソース(たとえば、投稿、コメントなど)のエントリのリストを取得します。







 // List.js import React from 'react'; import { connect } from 'react-redux'; const List = (props) => ... const mapStateToProps = (state, props) => { const resourceState = state.admin[props.resource]; return { ids: resourceState.list.ids, data: Object.keys(resourceState.data) .filter(id => resourceState.list.ids.includes(id)) .map(id => resourceState.data[id]) .reduce((data, record) => { data[record.id] = record; return data; }, {}), }; }; export default connect(mapStateToProps)(List);
      
      





状態には、リソースによってインデックスが付けられた以前にロードされたすべてのレコードの配列が含まれます。 たとえば、 state.admin.posts.data



には投稿のリストが含まれます。







 { 23: { id: 23, title: “Hello, World”, /* … */ }, 45: { id: 45, title: “Lorem Ipsum”, /* … */ }, 67: { id: 67, title: “Sic dolor amet”, /* … */ }, }
      
      





mapStateToProps



関数は、状態オブジェクトをフィルタリングし、実際にリストに表示されるレコードのみを返します。 このようなもの:







 { 23: { id: 23, title: “Hello, World”, /* … */ }, 67: { id: 67, title: “Sic dolor amet”, /* … */ },\ }
      
      





問題は、 mapStateToProps



関数がmapStateToProps



れるたびに、内部オブジェクトが変更されていなくても、新しいオブジェクトが返されることです。 結果として、 <List>



コンポーネントは、何かが状態が変化するたびに再描画されます。一方、日付またはIDを変更する場合は、idのみを変更する必要があります。







再選択はメモ化でこの問題を解決します。 mapStateToProps



で小道具を直接計算する代わりに、reselectのセレクターを使用します。これは、変更が行われなかった場合に同じオブジェクトを返します。







 import React from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect' const List = (props) => ... const idsSelector = (state, props) => state.admin[props.resource].ids const dataSelector = (state, props) => state.admin[props.resource].data const filteredDataSelector = createSelector( idsSelector, dataSelector (ids, data) => Object.keys(data) .filter(id => ids.includes(id)) .map(id => data[id]) .reduce((data, record) => { data[record.id] = record; return data; }, {}) ) const mapStateToProps = (state, props) => { const resourceState = state.admin[props.resource]; return { ids: idsSelector(state, props), data: filteredDataSelector(state, props), }; }; export default connect(mapStateToProps)(List);
      
      





<List>



コンポーネントは、状態のサブセットが変更されたときにのみ再描画されるようになりました。







再構成に関しては、セレクターはテストとビルドが簡単な純粋な関数です。 Reduxに接続されているコンポーネントのセレクターを作成することをお勧めします。







JSXのオブジェクトリテラルに注意してください



ある日、コンポーネントはさらに「クリーン」になり、コードに悪いパターンが見つかり、無駄な再描画につながる可能性があります。 これの最も一般的な例は、JSXでのオブジェクトリテラルの使用です。これは、「 Notorious {{ 」と呼びます。 例を挙げましょう。







 import React from 'react'; import MyTableComponent from './MyTableComponent'; const Datagrid = (props) => ( <MyTableComponent style={{ marginTop: 10 }}> ... </MyTableComponent> )
      
      





<MyTableComponent>



コンポーネントのstyle



プロパティは、 <Datagrid>



コンポーネントが描画されるたびに新しい値を取得します。 したがって、 <MyTableComponent>



クリーンであっても、 <Datagrid>



再描画される<Datagrid>



再描画されます。 実際、オブジェクトリテラルをプロパティとして子コンポーネントに渡すたびに、純度に違反します。 解決策は簡単です。







 import React from 'react'; import MyTableComponent from './MyTableComponent'; const tableStyle = { marginTop: 10 }; const Datagrid = (props) => ( <MyTableComponent style={tableStyle}> ... </MyTableComponent> )
      
      





とても単純に見えますが、このエラーを何度も目にして、JSXで「悪名高い{{



」を発見する感覚を身に付けました。 定期的に定数に置き換えています。







次に疑われるコンポーネントの盗難はReact.CloneElement()



です。 プロパティを2番目のパラメーターの値として渡すと、クローン要素はレンダリングされるたびに新しい小道具を受け取ります。







 //  const MyComponent = (props) => <div>{React.cloneElement(Foo, { bar: 1 })}</div>; //  const additionalProps = { bar: 1 }; const MyComponent = (props) => <div>{React.cloneElement(Foo, additionalProps)}</div>;
      
      





例として次のコードを使用して、 マテリアルUIでこれに何度か火傷を負いました。







 import { CardActions } from 'material-ui/Card'; import { CreateButton, RefreshButton } from 'admin-on-rest'; const Toolbar = ({ basePath, refresh }) => ( <CardActions> <CreateButton basePath={basePath} /> <RefreshButton refresh={refresh} /> </CardActions> ); export default Toolbar;
      
      





<CreateButton>



コンポーネントはクリーンですが、 <Toolbar>



描画されるたびにレンダリングされます。 これは、material-uiの<CardAction>



コンポーネントが、マージンに配置される最初の子に特別なスタイルを追加し、オブジェクトリテラルを使用してこれを行うためです。 したがって、 <CreateButton>



は毎回異なるstyle



オブジェクトを取得します。 再構成からHOC関数onlyUpdateForKeys()



を使用してこれを解決することができました。







 // Toolbar.js import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; const Toolbar = ({ basePath, refresh }) => ( ... ); export default onlyUpdateForKeys(['basePath', 'refresh'])(Toolbar);
      
      





おわりに



Reactアプリケーションを高速に保つために必要なことは他にもたくさんあります(キーの使用、重いルートの遅延読み込み、 react-addons-perf



addons react-addons-perf



パッケージ、ServiceWorkersによるアプリケーションの状態のキャッシュ、同型の追加など)が、正しい実装shouldComponentUpdate



は、最初の最も効果的な手順です。







React単独では高速ではありませんが、あらゆるサイズのアプリケーションを高速にするためのすべてのツールを提供します。







特に多くのフレームワークがReactに代わるものを提供し、N倍高速であると主張する場合、これは直感に反するように思われます。 しかし、Reactはパフォーマンスではなく、開発者の使いやすさと経験に焦点を当てています。 これが、Reactを使用した大規模なアプリケーションの開発が快適な体験であり、驚くことなく、実装の安定したペースである理由です。







時々アプリケーションのプロファイルを作成し、必要に応じてpure()



呼び出しの追加に時間をかけることを忘れないでください。 ただし、最初はこれを行わないでください。また、各コンポーネントを最適化するのに時間をかけすぎないでください。ただし、モバイルデバイスで行わない場合は除きます。 また、ユーザーの観点からアプリケーションの応答性について良い印象を得るために、さまざまなデバイスでテストすることを忘れないでください。







Reactのパフォーマンスの最適化について詳しく知りたい場合は、このトピックに関する優れた記事のリストをご覧ください。










All Articles