イベントハンドラーのキャッシュとReactアプリケーションのパフォーマンスの向上

本日、マテリアルの翻訳を公開します。著者は、JavaScriptでオブジェクトを操作する機能を分析し、React開発者にアプリケーションを高速化する方法を提供しています。 特に、「オブジェクトを割り当てる」変数であり、単に「オブジェクト」と呼ばれることが多い変数が、実際にはオブジェクト自体ではなく、そのオブジェクトへのリンクであるという事実について話しています。 JavaScriptの関数もオブジェクトであるため、これらは関数に当てはまります。 これを念頭に置いて、Reactコンポーネントを設計し、コードを厳密に分析すると、内部メカニズムを改善し、アプリケーションのパフォーマンスを改善できます。



JavaScriptでオブジェクトを操作する機能


まったく同じように見えるいくつかの関数を作成して比較しようとすると、システムの観点からは異なることがわかります。 これを確認するには、次のコードを実行できます。

const functionOne = function() { alert('Hello world!'); }; const functionTwo = function() { alert('Hello world!'); }; functionOne === functionTwo; // false 

次に、別の変数に既に割り当てられている既存の関数に変数を割り当てて、これら2つの変数を比較してみましょう。

 const functionThree = function() { alert('Hello world!'); }; const functionFour = functionThree; functionThree === functionFour; // true 

ご覧のとおり、このアプローチでは、厳密な等価演算子はtrue返しtrue
オブジェクトは自然に同じように振る舞います:

 const object1 = {}; const object2 = {}; const object3 = object1; object1 === object2; // false object1 === object3; // true 

ここではJavaScriptについて説明していますが、他の言語で開発した経験がある場合は、ポインターの概念に精通しているかもしれません。 上記のコードでは、オブジェクトが作成されるたびに、システムメモリの一部がそのオブジェクトに割り当てられます。 object1 = {}形式のコマンドを使用すると、 object1 = {}専用に割り当てられたメモリの一部が何らかのデータでobject1ます。

object1を、オブジェクトに関連するデータ構造がメモリ内にあるアドレスとして想像することは非常に可能です。 コマンドobject2 = {}実行すると、特にobject2専用の別のメモリ領域が割り当てられます。 obect1obect1は同じメモリ領域にありますか? いいえ、それぞれに独自のプロットがあります。 そのため、 object1object1を比較しようとするとfalseになりfalse 。 これらのオブジェクトは同じ構造を持つ場合がありますが、それらが配置されているメモリ内のアドレスは異なり、比較中にチェックされるのはアドレスです。

コマンドobject3 = object1実行することにより、 object1のアドレスをobject3定数に書き込みます。 これは新しいオブジェクトではありません。 この定数には、既存のオブジェクトのアドレスが割り当てられます。 これは次の方法で確認できます。

 const object1 = { x: true }; const object3 = object1; object3.x = false; object1.x; // false 

この例では、オブジェクトがメモリ内に作成され、そのアドレスが定数object1書き込まれます。 次に、同じアドレスが定数object3書き込まれます。 object3変更すると、メモリ内のオブジェクトが変更されます。 これは、たとえばobject1に格納されているオブジェクトなど、他の参照を使用してオブジェクトにアクセスする場合、変更されたバージョンで既に作業することをobject1ます。

関数、オブジェクト、React


初心者の開発者が上記のメカニズムを誤解すると、多くの場合エラーが発生します。おそらく、オブジェクトを操作する機能を検討することは別の記事に値します。 ただし、今日のトピックはReactアプリケーションのパフォーマンスです。 この領域では、JavaScript変数と定数がオブジェクト自体に保存されず、それらへのリンクのみであるという事実によってReactアプリケーションがどのように影響を受けるかに注意を払っていない、かなり経験豊富な開発者でも間違いを犯す可能性があります。

これはReactと何の関係がありますか? Reactには、アプリケーションのパフォーマンスを向上させることを目的としたシステムリソースを節約するためのインテリジェントなメカニズムがあります。コンポーネントのプロパティと状態が変わらない場合、 render機能は変わりません。 明らかに、コンポーネントが同じままであれば、再度レンダリングする必要はありません。 何も変化しない場合、 render関数は以前と同じものを返すため、実行する必要はありません。 このメカニズムにより、Reactは高速になります。 必要な場合にのみ何かが表示されます。

Reactは、標準のJavaScript機能を使用してコンポーネントのプロパティと状態が等しいかどうかをチェックします。つまり、 ==演算子を使用してそれらを単純に比較します。 Reactは、オブジェクトの等価性を判断するために、オブジェクトの「浅い」または「深い」比較を実行しません。 浅い比較は、メモリ内のオブジェクトのアドレスのみを比較する比較(オブジェクトへの参照)とは対照的に、オブジェクトの各キーと値のペアの比較を記述するために使用される概念です。 オブジェクトの「詳細な」比較はさらに進んでおり、オブジェクトの比較されたプロパティの値もオブジェクトである場合、これらのオブジェクトのキーと値のペアも比較されます。 このプロセスは、他のオブジェクトにネストされているすべてのオブジェクトに対して繰り返されます。 Reactはこの種のことは何もせず、リンクの等価性をチェックするだけです。

たとえば、フォーム{ x: 1 }オブジェクトで表されるコンポーネントのプロパティをまったく同じように見える別のオブジェクトに変更すると、これらのオブジェクトは異なるメモリ領域にあるため、Reactはコンポーネントを再レンダリングします。 上記の例を思い出すと、コンポーネントのプロパティをobject3からobject1に変更するとき、定数object1object3は同じオブジェクトを参照するため、Reactはそのようなコンポーネントを再レンダリングしません。

JavaScriptでの関数の操作は、まったく同じ方法で構成されています。 Reactが異なるアドレスで同じ機能に遭遇した場合、再レンダリングされます。 「新しい関数」が、すでに使用されている関数への単なるリンクである場合、再レンダリングは行われません。

コンポーネントを扱う際の典型的な問題


コンポーネントを操作するシナリオの1つを次に示します。残念ながら、他の人のコードをチェックするときは常に私に出くわします。

 class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={() => alert('!')} />     </div>   ); } } 

前にあるのは非常に単純なコンポーネントです。 これはボタンであり、クリックすると通知が表示されます。 ボタンの横には、使用方法が表示され、このボタンを押すかどうかをユーザーに通知します。 それらはSomeComponentコンポーネントのdodo={true}またはdo={false}SomeComponent設定することで、表示される表示を正確に制御します。

SomeComponentコンポーネントが再レンダリングされるSomeComponentdoプロパティの値がtrueからfalse 、またはその逆に変更される場合)、 Buttonエレメントもレンダリングされます。 onClickハンドラーは常に同じですが、 render関数が呼び出されるたびに再作成されます。 その結果、コンポーネントがメモリに表示されるたびに、新しい関数が作成されます。その作成はrender関数で実行されるrender 、メモリ内の新しいアドレスへのリンクが<Button />渡され、 Buttonコンポーネントも再レンダリングされますが、何も変わっていません。

修正方法について話します。

問題解決


関数がコンポーネント( thisコンテキスト)から独立している場合、コンポーネントの外部で定義できます。 すべての場合で同じ関数になるため、コンポーネントのすべてのインスタンスは同じ関数参照を使用します。 これは次のようなものです。

 const createAlertBox = () => alert('!'); class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={createAlertBox} />     </div>   ); } } 

前の例とは異なり、 createAlertBoxにはrender呼び出すたびに、メモリ内の同じ領域への同じリンクが含まれます。 その結果、 Button繰り返し出力は実行されません。

Buttonコンポーネントは小さく、すばやくレンダリングされますが、関数の内部宣言に関連する上記の問題は、レンダリングに多くの時間を要する大きくて複雑なコンポーネントにも見られます。 これにより、Reactアプリケーションの速度が大幅に低下する可能性があります。 この点で、そのような関数をrenderメソッド内で宣言するべきではないという推奨事項に従うことは理にかなっていrender

関数がコンポーネントに依存している場合、つまり、関数を外部で定義できない場合、コンポーネントメソッドをイベントハンドラとして渡すことができます。

 class SomeComponent extends React.PureComponent { createAlertBox = () => {   alert(this.props.message); }; get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={this.createAlertBox} />     </div>   ); } } 

この場合、 SomeComponent各インスタンスで、ボタンをクリックすると、さまざまなメッセージが表示されます。 Button要素のイベントハンドラーはSomeComponentに一意である必要があります。 cteateAlertBoxメソッドを渡すとき、 SomeComponent再レンダリングされるかどうかは関係ありません。 messageプロパティが変更されたかどうかは関係ありません。 createAlertBox関数のアドレスは変更されません。つまり、 Button要素を再度レンダリングすることはできません。 これにより、システムリソースを節約し、アプリケーションのレンダリング速度を向上させることができます。

これはすべて良いことです。 しかし、関数が動的な場合はどうでしょうか?

より複雑な問題を解決する


この資料の著者は、関数の再利用を説明するのに適した、最初に頭に浮かんだものを取り上げて、このセクションの例を準備したことに注意してください。 これらの例は、読者がアイデアの本質を理解するのに役立つことを目的としています。 何が起こっているのかを理解するためにこのセクションを読むことをお勧めしますが、Reactに組み込まれているキャッシュ無効化とメモリ管理メカニズムの機能を考慮した、ここで説明したメカニズムのより良いバージョンを提案した読者がいるため、著者は元の記事のコメントに注意することをお勧めします。

そのため、1つのコンポーネントには多くのユニークな動的イベントハンドラーが存在することは非常に一般的です。たとえば、同様のことがコードで見られ、 renderメソッドでmap配列メソッドが使用されrender

 class SomeComponent extends React.PureComponent { render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={() => alert(listItem.text)} />         </li>       )}     </ul>   ); } } 

ここでは、異なる数のボタンが表示され、異なる数のイベントハンドラーが作成されます。各イベントハンドラーは一意の関数で表されます。事前にSomeComponent作成するSomeComponent 、これらの関数が何であるかはSomeComponentません。 このパズルを解決するには?

ここでメモ化は、私たち、またはより簡単にキャッシュに役立ちます。 一意の値ごとに、関数を作成してキャッシュに入れます。 この一意の値が再び発生する場合、以前にキャッシュに配置されたそれに対応する関数をキャッシュから取得するだけで十分です。

このアイデアの実装は次のようになります。

 class SomeComponent extends React.PureComponent { //    SomeComponent        //   . clickHandlers = {}; //       //    . getClickHandler(key) {   //       ,  .   if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {     this.clickHandlers[key] = () => alert(key);   }   return this.clickHandlers[key]; } render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={this.getClickHandler(listItem.text)} />         </li>       )}     </ul>   ); } } 

配列の各要素は、 getClickHandlerメソッドによって処理されます。 このメソッドは、特定の値で初めて呼び出されたときに、この値に固有の関数を作成し、キャッシュに入れて返します。 同じ値を渡してこのメ​​ソッドを呼び出すと、キャッシュから関数へのリンクが単純に返されます。

その結果、 SomeComponentを再レンダリングしてもButtonは再レンダリングされません。 同様に、 listプロパティに要素を追加すると、各ボタンのイベントハンドラーが動的に作成されます。

ハンドラーが複数の変数で定義されている場合、ハンドラーの一意の識別子を作成するには創造的である必要がありますが、これはmapメソッドの結果として取得された各JSXオブジェクトの一意のkeyプロパティの通常の作成よりもそれほど複雑ではありません。

ここで、配列インデックスを識別子として使用する際に起こりうる問題について警告したいと思います。 実際、このアプローチでは、配列内の要素の順序が変更されたり、要素の一部が削除されたりすると、エラーが発生する可能性があります。 したがって、たとえば、最初にそのような配列が[ 'soda', 'pizza' ]ように見えてから[ 'pizza' ]に変わり、フォームlisteners[0] = () => alert('soda')コマンドを使用してイベントハンドラーをキャッシュした場合listeners[0] = () => alert('soda') 、ユーザーが識別子0のハンドラーが割り当てられ、 [ 'pizza' ]配列の内容に従ってpizzaメッセージを表示するボタンをクリックすると、 sodaメッセージが表示されることがわかります。 同じ理由で、キープロパティとして配列インデックスを使用することは推奨されません。

まとめ


この記事では、Reactアプリケーションのレンダリングを高速化できることを考慮して、内部JavaScriptメカニズムの機能を調べました。 ここで紹介したアイデアが役に立つことを願っています。

親愛なる読者! Reactアプリケーションを最適化するための興味深い方法を知っているなら、それらを共有してください。

Source: https://habr.com/ru/post/J426053/


All Articles