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シリーズのコーディング方法ページに戻ってください。