Reactwithmemo、useMemo、useCallbackでパフォーマンスの落とし穴を回避する方法
著者は、 Creative Commons を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
React アプリケーションでは、パフォーマンスの問題は、ネットワークレイテンシ、過負荷のAPI、非効率的なサードパーティライブラリ、さらには異常に大きな負荷が発生するまで正常に機能する適切に構造化されたコードに起因する可能性があります。 パフォーマンスの問題の根本原因を特定するのは難しい場合がありますが、これらの問題の多くはコンポーネントの再レンダリングに起因します。 コンポーネントが予想以上に再レンダリングするか、コンポーネントにデータ量の多い操作があり、各レンダリングが遅くなる可能性があります。 このため、不要な再レンダリングを防ぐ方法を学ぶことで、Reactアプリケーションのパフォーマンスを最適化し、ユーザーのエクスペリエンスを向上させることができます。
このチュートリアルでは、Reactコンポーネントのパフォーマンスの最適化に焦点を当てます。 問題を調査するために、テキストのブロックを分析するコンポーネントを作成します。 さまざまなアクションによって再レンダリングがトリガーされる方法と、フックおよびメモ化を使用してコストのかかるデータ計算を最小限に抑える方法について説明します。 このチュートリアルを終えると、useMemo
やuseCallback
フックなど、パフォーマンスを向上させる多くのフックと、それらを必要とする状況に慣れることができます。
前提条件
- Node.jsを実行する開発環境が必要になります。 このチュートリアルは、Node.jsバージョン10.22.0およびnpmバージョン6.14.6でテストされました。 これをmacOSまたはUbuntu18.04にインストールするには、Node.jsをインストールしてmacOSにローカル開発環境を作成する方法またはのPPAを使用したインストール]セクションの手順に従います。 Ubuntu18.04にNode.jsをインストールする方法。
- Create React App でセットアップされたReact開発環境で、不要なボイラープレートが削除されています。 これを設定するには、ステップ1 —Reactクラスコンポーネントの状態を管理する方法のチュートリアルの空のプロジェクトを作成します。 このチュートリアルでは、プロジェクト名として
performance-tutorial
を使用します。 - Reactでのデバッグに慣れていない場合は、チュートリアル React Developer Tools を使用してReactコンポーネントをデバッグする方法を確認し、ChromeDevToolsなどの使用しているブラウザーの開発者ツールに慣れてください。 およびFirefox開発者ツール。
- また、JavaScript、HTML、およびCSSの基本的な知識も必要です。これは、HTMLシリーズを使用してWebサイトを構築する方法、CSSシリーズを使用してWebサイトを構築する方法にあります。 、およびJavaScriptでコーディングする方法。
ステップ1—memo
による再レンダリングの防止
このステップでは、コンポーネントを分析するテキストを作成します。 テキストのブロックを取得するための入力と、文字と記号の頻度を計算するコンポーネントを作成します。 次に、テキストアナライザーのパフォーマンスが低下するシナリオを作成し、パフォーマンスの問題の根本的な原因を特定します。 最後に、React memo
関数を使用して、親が変更されたときにコンポーネントが再レンダリングされないようにしますが、子コンポーネントへのpropsは変更されません。
このステップを完了すると、チュートリアルの残りの部分で使用する作業コンポーネントが完成し、親の再レンダリングによって子コンポーネントのパフォーマンスの問題がどのように発生するかを理解できます。
テキストアナライザーの構築
まず、<textarea>
要素をApp.js
に追加します。
選択したテキストエディタでApp.js
を開きます。
nano src/components/App/App.js
次に、<textarea>
入力と<label>
を追加します。 次の強調表示されたコードを追加して、<div>
内に配置します。
パフォーマンス-チュートリアル/src/components/App/App.js
import React from 'react'; import './App.css'; function App() { return( <div className="wrapper"> <label htmlFor="text"> <p>Add Your Text Here:</p> <textarea id="text" name="text" rows="10" cols="100" > </textarea> </label> </div> ) } export default App;
これにより、サンプルアプリケーションの入力ボックスが作成されます。 ファイルを保存して閉じます。
App.css
を開いて、いくつかのスタイルを追加します。
nano src/components/App/App.css
App.css
内で、.wrapper
クラスにパディングを追加してから、[X85X]をdiv
要素に追加します。 CSSを次のように置き換えます。
パフォーマンス-チュートリアル/src/components/App/App.css
.wrapper { padding: 20px; } .wrapper div { margin: 20px 0; }
これにより、入力とデータ表示が分離されます。 ファイルを保存して閉じます。
次に、CharacterMap
コンポーネント用のディレクトリを作成します。 このコンポーネントは、テキストを分析し、各文字と記号の頻度を計算し、結果を表示します。
まず、ディレクトリを作成します。
mkdir src/components/CharacterMap
次に、テキストエディタでCharacterMap.js
を開きます。
nano src/components/CharacterMap/CharacterMap.js
内部で、text
を小道具として取り、<div>
内にtext
を表示するCharacterMap
というコンポーネントを作成します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React from 'react'; import PropTypes from 'prop-types'; export default function CharacterMap({ text }) { return( <div> Character Map: {text} </div> ) } CharacterMap.propTypes = { text: PropTypes.string.isRequired }
text
小道具にPropTypeを追加して、弱い型付けを導入していることに注意してください。
テキストをループして文字情報を抽出する関数を追加します。 関数にitemize
という名前を付け、text
を引数として渡します。 itemize
関数は、すべての文字を数回ループし、テキストサイズが大きくなると非常に遅くなります。 これにより、パフォーマンスをテストするための良い方法になります。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React from 'react'; import PropTypes from 'prop-types'; function itemize(text){ const letters = text.split('') .filter(l => l !== ' ') .reduce((collection, item) => { const letter = item.toLowerCase(); return { ...collection, [letter]: (collection[letter] || 0) + 1 } }, {}) return letters; } export default function CharacterMap({ text }) { return( <div> Character Map: {text} </div> ) } CharacterMap.propTypes = { text: PropTypes.string.isRequired }
itemize
内では、すべての文字で .split を使用して、テキストを配列に変換します。 次に、 .filterメソッドを使用してスペースを削除し、.reduceメソッドを使用して各文字を繰り返し処理します。 .reduce
メソッド内で、 object を初期値として使用し、文字を小文字に変換して、前の合計または0
以前の合計がなかった場合。 オブジェクトスプレッド演算子を使用して以前の値を保持しながら、オブジェクトを新しい値で更新します。
各文字のカウントを持つオブジェクトを作成したので、最も高い文字でオブジェクトを並べ替える必要があります。 オブジェクトをObject.entries
を使用してペアの配列に変換します。 配列の最初の項目は文字で、2番目の項目はカウントです。 .sort メソッドを使用して、最も一般的な文字を上に配置します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React from 'react'; import PropTypes from 'prop-types'; function itemize(text){ const letters = text.split('') .filter(l => l !== ' ') .reduce((collection, item) => { const letter = item.toLowerCase(); return { ...collection, [letter]: (collection[letter] || 0) + 1 } }, {}) return Object.entries(letters) .sort((a, b) => b[1] - a[1]); } export default function CharacterMap({ text }) { return( <div> Character Map: {text} </div> ) } CharacterMap.propTypes = { text: PropTypes.string.isRequired }
最後に、テキストを使用してitemize
関数を呼び出し、結果を表示します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React from 'react'; import PropTypes from 'prop-types'; function itemize(text){ const letters = text.split('') .filter(l => l !== ' ') .reduce((collection, item) => { const letter = item.toLowerCase(); return { ...collection, [letter]: (collection[letter] || 0) + 1 } }, {}) return Object.entries(letters) .sort((a, b) => b[1] - a[1]); } export default function CharacterMap({ text }) { return( <div> Character Map: {itemize(text).map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { text: PropTypes.string.isRequired }
ファイルを保存して閉じます。
次に、コンポーネントをインポートして、App.js
内にレンダリングします。 App.js
を開きます:
nano src/components/App/App.js
コンポーネントを使用する前に、テキストを保存する方法が必要です。 useState をインポートしてから関数を呼び出し、text
という変数とsetText
という更新関数に値を格納します。
text
を更新するには、event.target.value
をsetText
関数に渡す関数をonChange
に追加します。
パフォーマンス-チュートリアル/src/components/App/App.js
import React, { useState } from 'react'; import './App.css'; function App() { const [text, setText] = useState(''); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> </div> ) } export default App;
空の文字列でuseState
を初期化していることに注意してください。 これにより、ユーザーがまだテキストを入力していなくても、CharacterMap
コンポーネントに渡す値が常に文字列であることが保証されます。
CharacterMap
をインポートし、<label>
要素の後にレンダリングします。 text
状態をtext
プロップに渡します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <CharacterMap text={text} /> </div> ) } export default App;
ファイルを保存します。 これを行うと、ブラウザが更新され、テキストを追加すると、入力後に文字分析が表示されます。
例に示されているように、コンポーネントは少量のテキストでかなりうまく機能します。 キーストロークごとに、ReactはCharacterMap
を新しいデータで更新します。 ただし、ローカルでのパフォーマンスは誤解を招く可能性があります。 すべてのデバイスが開発環境と同じメモリを備えているわけではありません。
パフォーマンスのテスト
アプリケーションのパフォーマンスをテストする方法は複数あります。 大量のテキストを追加することも、使用するメモリを減らすようにブラウザを設定することもできます。 コンポーネントをパフォーマンスのボトルネックにプッシュするには、GNUのWikipediaエントリをコピーして、テキストボックスに貼り付けます。 ウィキペディアのページの編集方法によって、サンプルが若干異なる場合があります。
エントリをテキストボックスに貼り付けた後、追加の文字e
を入力してみて、表示にかかる時間を確認してください。 文字コード表が更新される前に、かなりの一時停止があります。
コンポーネントの速度が十分でなく、 Firefox 、 Edge 、またはその他のブラウザを使用している場合は、速度が低下するまでテキストを追加してください。
Chrome を使用している場合は、[パフォーマンス]タブ内でCPUを調整できます。 これは、スマートフォンや古いハードウェアをエミュレートするのに最適な方法です。 詳細については、ChromeDevToolsのドキュメントをご覧ください。
ウィキペディアのエントリでコンポーネントが遅すぎる場合は、一部のテキストを削除してください。 目立った遅延を受け取りたいが、それを異常に遅くしたり、ブラウザをクラッシュさせたりしたくない。
子コンポーネントの再レンダリングの防止
itemize
関数は、前のセクションで特定された遅延のルートです。 この関数は、コンテンツを数回ループすることにより、各エントリに対して多くの作業を実行します。 関数自体で直接実行できる最適化がありますが、このチュートリアルの焦点は、テキストが変更されていないときにコンポーネントの再レンダリングを処理する方法です。 つまり、itemize
関数は、変更するためのアクセス権がない関数として扱われます。 目標は、必要な場合にのみ実行することです。 これは、制御できないAPIまたはサードパーティライブラリのパフォーマンスを処理する方法を示しています。
まず、親は変更されますが、子コンポーネントは変更されない状況を調べます。
App.js
内に、コンポーネントの動作を説明する段落と、情報を切り替えるボタンを追加します。
パフォーマンス-チュートリアル/src/components/App/App.js
import React, { useReducer, useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); const [showExplanation, toggleExplanation] = useReducer(state => !state, false) return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <div> <button onClick={toggleExplanation}>Show Explanation</button> </div> {showExplanation && <p> This displays a list of the most common characters. </p> } <CharacterMap text={text} /> </div> ) } export default App;
レデューサー機能付きのuseReducer
フックを呼び出して、現在の状態を反転させます。 出力をshowExplanation
およびtoggleExplanation
に保存します。 <label>
の後に、説明を切り替えるボタンと、showExplanation
が正しい場合に表示される段落を追加します。
ファイルを保存して終了します。 ブラウザが更新されたら、ボタンをクリックして説明を切り替えます。 遅延があることに注意してください。
これには問題があります。 ユーザーが少量のJSXを切り替えているときに、遅延が発生することはありません。 親コンポーネントが変更されると(この状況ではApp.js
)、CharacterMap
コンポーネントが文字データを再レンダリングおよび再計算するため、遅延が発生します。 text
プロップは同じですが、親が変更されたときにReactがコンポーネントツリー全体を再レンダリングするため、コンポーネントは引き続き再レンダリングされます。
ブラウザの開発者ツールを使用してアプリケーションのプロファイルを作成すると、親が変更されたためにコンポーネントが再レンダリングされることがわかります。 開発者ツールを使用したプロファイリングのレビューについては、 ReactDeveloperToolsを使用してReactコンポーネントをデバッグする方法を確認してください。
CharacterMap
には高価な関数が含まれているため、小道具が変更された場合にのみ再レンダリングする必要があります。
CharacterMap.js
を開きます:
nano src/components/CharacterMap/CharacterMap.js
次に、memo
をインポートし、コンポーネントをmemo
に渡して、結果をデフォルトとしてエクスポートします。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { memo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ ... } function CharacterMap({ text }) { return( <div> Character Map: {itemize(text).map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { text: PropTypes.string.isRequired } export default memo(CharacterMap);
ファイルを保存します。 これを行うと、ブラウザがリロードされ、ボタンをクリックしてから結果が得られるまでの遅延はなくなります。
開発者ツールを見ると、コンポーネントが再レンダリングされていないことがわかります。
memo
関数は、小道具の浅い比較を実行し、小道具が変更された場合にのみ再レンダリングします。 浅い比較では、===
演算子を使用して、前の小道具を現在の小道具と比較します。
比較は無料ではないことを覚えておくことが重要です。 小道具をチェックするにはパフォーマンスコストがかかりますが、計算に費用がかかるなど、パフォーマンスに明らかな影響がある場合は、再レンダリングを防ぐ価値があります。 さらに、Reactは浅い比較を実行するため、プロップがオブジェクトまたは関数である場合でも、コンポーネントは再レンダリングされます。 ステップ3で、小道具としての関数の処理について詳しく説明します。
このステップでは、計算に時間がかかり、時間がかかるアプリケーションを作成しました。 親の再レンダリングによって子コンポーネントが再レンダリングされる方法と、memo
を使用して再レンダリングを防止する方法を学習しました。 次のステップでは、コンポーネント内のアクションをメモ化して、特定のプロパティが変更されたときにのみアクションを実行するようにします。
ステップ2—useMemo
を使用した高価なデータ計算のキャッシュ
このステップでは、useMemo
フックを使用して低速データ計算の結果を保存します。 次に、useMemo
フックを既存のコンポーネントに組み込み、データの再計算の条件を設定します。 この手順を完了すると、高価な関数をキャッシュして、特定のデータが変更された場合にのみ実行されるようになります。
前のステップでは、コンポーネントの切り替えられた説明は親の一部でした。 ただし、代わりにCharacterMap
コンポーネント自体に追加することもできます。 その場合、CharacterMap
にはtext
とshowExplanation
の2つのプロパティがあり、showExplanation
が正しい場合に説明が表示されます。
開始するには、CharacterMap.js
を開きます。
nano src/components/CharacterMap/CharacterMap.js
CharacterMap
内に、showExplanation
の新しいプロパティを追加します。 showExplanation
の値が正しい場合は、説明テキストを表示します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { memo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ ... } function CharacterMap({ showExplanation, text }) { return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {itemize(text).map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default memo(CharacterMap);
ファイルを保存して閉じます。
次に、App.js
を開きます。
nano src/components/App/App.js
説明の段落を削除し、showExplanation
を小道具としてCharacterMap
に渡します。
パフォーマンス-チュートリアル/src/components/App/App.js
import React, { useReducer, useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); const [showExplanation, toggleExplanation] = useReducer(state => !state, false) return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <div> <button onClick={toggleExplanation}>Show Explanation</button> </div> <CharacterMap showExplanation={showExplanation} text={text} /> </div> ) } export default App;
ファイルを保存して閉じます。 これを行うと、ブラウザが更新されます。 説明を切り替えると、再び遅延が発生します。
プロファイラーを見ると、showExplanation
プロペラが変更されたため、コンポーネントが再レンダリングされたことがわかります。
memo
関数は小道具を比較し、小道具が変更されない場合は再レンダリングを防ぎますが、この場合、showExplanation
小道具は変更されるため、コンポーネント全体が再レンダリングされ、コンポーネントが再レンダリングされます。 itemize
機能を実行します。
この場合、コンポーネント全体ではなく、コンポーネントの特定の部分をメモ化する必要があります。 Reactは、useMemo
と呼ばれる特別なフックを提供します。これを使用して、再レンダリング間でコンポーネントの一部を保持できます。 フックは2つの引数を取ります。 最初の引数は、メモ化する値を返す関数です。 2番目の引数は、依存関係の配列です。 依存関係が変更された場合、useMemo
は関数を再実行し、値を返します。
useMemo
を実装するには、最初にCharacterMap.js
を開きます。
nano src/components/CharacterMap/CharacterMap.js
characters
という新しい変数を宣言します。 次に、useMemo
を呼び出し、itemize(text)
の値を最初の引数として返し、text
を2番目の引数として含む配列を返す無名関数を渡します。 useMemo
を実行すると、itemize(text)
の結果がcharacters
変数に返されます。
JSXのitemize
への呼び出しをcharacters
に置き換えます。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ ... } function CharacterMap({ showExplanation, text }) { const characters = useMemo(() => itemize(text), [text]); return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {characters.map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default memo(CharacterMap);
ファイルを保存します。 これを行うと、ブラウザがリロードされ、説明を切り替えても遅延は発生しません。
コンポーネントのプロファイルを作成すると、コンポーネントが再レンダリングされることがわかりますが、レンダリングにかかる時間ははるかに短くなります。 この例では、useMemo
フックがない場合の916.4ミリ秒と比較して、0.7ミリ秒かかりました。 これは、Reactがコンポーネントを再レンダリングしているが、useMemo
フックに含まれている関数を再実行していないためです。 コンポーネントの他の部分を更新できるようにしながら、結果を保持することができます。
テキストボックスのテキストを変更しても、依存関係(text
)が変更されたため、遅延が発生するため、useMemo
は関数を再実行します。 再実行されなかった場合は、古いデータがあります。 重要な点は、必要なデータが変更された場合にのみ実行されるということです。
このステップでは、コンポーネントの一部をメモ化しました。 高価な関数をコンポーネントの残りの部分から分離し、useMemo
フックを使用して、特定の依存関係が変更された場合にのみ関数を実行しました。 次のステップでは、浅い比較の再レンダリングを防ぐために関数をメモします。
ステップ3—useCallback
を使用した機能の同等性チェックの管理
このステップでは、JavaScriptで比較するのが難しい小道具を処理します。 小道具が変更されると、Reactは厳密な同等性チェックを使用します。 このチェックにより、フックを再実行するタイミングとコンポーネントを再レンダリングするタイミングが決まります。 JavaScriptの関数とオブジェクトを比較するのは難しいため、小道具は事実上同じであるにもかかわらず、再レンダリングをトリガーする状況があります。
useCallback
フックを使用して、再レンダリング間で機能を保持できます。 これにより、親コンポーネントが関数を再作成するときに不要な再レンダリングが防止されます。 この手順を完了すると、useCallback
フックを使用して再レンダリングを防ぐことができるようになります。
CharacterMap
コンポーネントを作成するときに、より柔軟にする必要がある場合があります。 itemize
関数では、常に文字を小文字に変換しますが、コンポーネントの一部のコンシューマーはその機能を望まない場合があります。 大文字と小文字を比較したり、すべての文字を大文字に変換したりする場合があります。
これを容易にするために、キャラクターを変更するtransformer
と呼ばれる新しい小道具を追加します。 transformer
関数は、引数として文字を受け取り、ある種の文字列を返すものになります。
CharacterMap
の中に、transformer
を小道具として追加します。 デフォルトでnull
のPropType
関数を指定します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text){ const letters = text.split('') .filter(l => l !== ' ') .reduce((collection, item) => { const letter = item.toLowerCase(); return { ...collection, [letter]: (collection[letter] || 0) + 1 } }, {}) return Object.entries(letters) .sort((a, b) => b[1] - a[1]); } function CharacterMap({ showExplanation, text, transformer }) { const characters = useMemo(() => itemize(text), [text]); return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {characters.map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired, transformer: PropTypes.func } CharacterMap.defaultProps = { transformer: null } export default memo(CharacterMap);
次に、itemize
を更新して、transformer
を引数として取ります。 .toLowerCase
メソッドをトランスに置き換えます。 transformer
が正しければ、item
を引数として関数を呼び出します。 それ以外の場合は、item
を返します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text, transformer){ const letters = text.split('') .filter(l => l !== ' ') .reduce((collection, item) => { const letter = transformer ? transformer(item) : item; return { ...collection, [letter]: (collection[letter] || 0) + 1 } }, {}) return Object.entries(letters) .sort((a, b) => b[1] - a[1]); } function CharacterMap({ showExplanation, text, transformer }) { ... } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired, transformer: PropTypes.func } CharacterMap.defaultProps = { transformer: null } export default memo(CharacterMap);
最後に、useMemo
フックを更新します。 transformer
を依存関係として追加し、それをitemize
関数に渡します。 依存関係が網羅的であることを確認する必要があります。 つまり、依存関係として変更される可能性のあるものをすべて追加する必要があります。 ユーザーが異なるオプションを切り替えてtransformer
を変更した場合は、関数を再実行して正しい値を取得する必要があります。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { memo, useMemo } from 'react'; import PropTypes from 'prop-types'; function itemize(text, transformer){ ... } function CharacterMap({ showExplanation, text, transformer }) { const characters = useMemo(() => itemize(text, transformer), [text, transformer]); return( <div> {showExplanation && <p> This display a list of the most common characters. </p> } Character Map: {characters.map(character => ( <div key={character[0]}> {character[0]}: {character[1]} </div> ))} </div> ) } CharacterMap.propTypes = { showExplanation: PropTypes.bool.isRequired, text: PropTypes.string.isRequired, transformer: PropTypes.func } CharacterMap.defaultProps = { transformer: null } export default memo(CharacterMap);
ファイルを保存して閉じます。
このアプリケーションでは、ユーザーが異なる機能を切り替えることができるようにしたくありません。 ただし、文字は小文字にする必要があります。 App.js
にtransformer
を定義して、文字を小文字に変換します。 この関数は変更されませんが、CharacterMap
に渡す必要があります。
App.js
を開きます:
nano src/components/App/App.js
次に、文字を小文字に変換するtransformer
という関数を定義します。 関数を小道具としてCharacterMap
に渡します。
パフォーマンス-チュートリアル/src/components/CharacterMap/CharacterMap.js
import React, { useReducer, useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); const [showExplanation, toggleExplanation] = useReducer(state => !state, false) const transformer = item => item.toLowerCase(); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <div> <button onClick={toggleExplanation}>Show Explanation</button> </div> <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} /> </div> ) } export default App;
ファイルを保存します。 そうすると、説明を切り替えると遅延が戻っていることがわかります。
コンポーネントのプロファイルを作成すると、小道具が変更され、フックが変更されたため、コンポーネントが再レンダリングされることがわかります。
よく見ると、showExplanation
の小道具が変更されていることがわかります。これは、ボタンをクリックしたため意味がありますが、transformer
の小道具も変更されています。
App
でボタンをクリックして状態を変更すると、App
コンポーネントが再レンダリングされ、transformer
が再宣言されました。 関数は同じですが、前の関数と参照的に同一ではありません。 つまり、前の関数と厳密には同じではありません。
次のコードブロックに示すように、ブラウザコンソールを開いて同じ関数を比較すると、比較が偽であることがわかります。
const a = () = {}; const b = () = {}; a === a // This will evaluate to true a === b // This will evaluate to false
===
比較演算子を使用すると、このコードは、2つの関数が同じ値であっても、等しいとは見なされないことを示しています。
この問題を回避するために、ReactはuseCallback
と呼ばれるフックを提供します。 フックはuseMemo
に似ています。最初の引数として関数を取り、2番目の引数として依存関係の配列を取ります。 違いは、useCallback
は関数を返し、関数の結果を返さないことです。 useMemo
フックと同様に、依存関係が変更されない限り、関数は再作成されません。 つまり、CharacterMap.js
のuseMemo
フックは同じ値を比較し、フックは再実行されません。
App.js
内で、useCallback
をインポートし、無名関数を最初の引数として渡し、空の配列を2番目の引数として渡します。 App
にこの関数を再作成させたくはありません。
パフォーマンス-チュートリアル/src/components/App/App.js
import React, { useCallback, useReducer, useState } from 'react'; import './App.css'; import CharacterMap from '../CharacterMap/CharacterMap'; function App() { const [text, setText] = useState(''); const [showExplanation, toggleExplanation] = useReducer(state => !state, false) const transformer = useCallback(item => item.toLowerCase(), []); return( <div className="wrapper"> <label htmlFor="text"> <p>Your Text</p> <textarea id="text" name="text" rows="10" cols="100" onChange={event => setText(event.target.value)} > </textarea> </label> <div> <button onClick={toggleExplanation}>Show Explanation</button> </div> <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} /> </div> ) } export default App;
ファイルを保存して閉じます。 そうすると、関数を再実行せずに説明を切り替えることができます。
コンポーネントのプロファイルを作成すると、フックが実行されなくなっていることがわかります。
この特定のコンポーネントでは、実際にはuseCallback
フックは必要ありません。 コンポーネントの外部で関数を宣言することができ、それが再レンダリングされることはありません。 ある種のpropまたはステートフルデータが必要な場合にのみ、コンポーネント内で関数を宣言する必要があります。 ただし、内部状態や小道具に基づいて関数を作成する必要がある場合があります。そのような状況では、useCallback
フックを使用して再レンダリングを最小限に抑えることができます。
このステップでは、useCallback
フックを使用して、再レンダリング間で機能を保持しました。 また、フックの小道具や依存関係と比較した場合に、これらの関数がどのように同等性を維持するかについても学びました。
結論
これで、高価なコンポーネントのパフォーマンスを向上させるためのツールが手に入りました。 memo
、useMemo
、およびuseCallback
を使用して、コストのかかるコンポーネントの再レンダリングを回避できます。 しかし、これらすべての戦略には、独自のパフォーマンスコストが含まれています。 memo
はプロパティを比較するために追加の作業を必要とし、フックは各レンダリングで追加の比較を実行する必要があります。 これらのツールは、プロジェクトに明確なニーズがある場合にのみ使用してください。そうしないと、独自のレイテンシが追加されるリスクがあります。
最後に、すべてのパフォーマンスの問題に技術的な修正が必要なわけではありません。 APIの速度が遅い、データ変換が大きいなど、パフォーマンスコストが避けられない場合があります。そのような状況では、読み込みコンポーネントをレンダリングするか、非同期関数の実行中にプレースホルダーを表示するか、その他のユーザー向けの機能強化を行うことで、デザインを使用して問題を解決できます。経験。
Reactチュートリアルをもっと読みたい場合は、 Reactトピックページを確認するか、React.jsシリーズのコーディング方法ページに戻ってください。