React、組み込みの機能とパフォーマンス

Reactについて話さなければならないとき、またはトレーニングコースの最初の講義を行い、あらゆる種類の興味深いことを示すとき、誰かが確かに尋ねます。 遅いと聞きました。」







この質問はいつも出てくるわけではありませんでしたが、ここ数か月間、図書館の作者と教師として、ほぼ毎日、時には講義で、時にはTwitterで答えなければなりません。 正直なところ、私はすでにこれにうんざりしています。 残念ながら、すべてを記事の形にする方が良いとすぐに気づきませんでした。これは、パフォーマンスの質問をしている人に役立つと思います。 実際、あなたの前には私の労苦の成果があります。



組み込み関数とは何ですか?



Reactのコンテキストでは、インライン関数と呼ばれるものは、レンダリングプロセス中に定義される関数です。 Reactでは、「レンダリング」の概念には2つの意味があり、しばしば混同されます。 1つ目は、アップグレードプロセス中にコンポーネントからReact要素を取得render



(コンポーネントrender



メソッドを呼び出す)ことに関するものです。 2番目は、DOMを変更することによるページフラグメントの実際の更新です。 この記事で「レンダリング」について話すとき、私は最初の選択肢を意味します。



組み込み関数の例を次に示します。



 class App extends Component { // ... render() {   return (     <div>             {/* 1.    " DOM" */}       <button         onClick={() => {           this.setState({ clicked: true })         }}       >         Click!       </button>             {/* 2. " "  "" */}       <Sidebar onToggle={(isOpen) => {         this.setState({ sidebarIsOpen: isOpen })       }}/>             {/* 3.   render */}       <Route         path="/topic/:id"         render={({ match }) => (           <div>             <h1>{match.params.id}</h1>}           </div>         )       />     </div>   ) } }
      
      





早すぎる最適化はすべての悪の根源です



続行する前に、プログラムを最適化する方法について話す必要があります。 パフォーマンスの専門家に尋ねると、彼は時期尚早な最適化は悪であると言うでしょう。 これは、絶対にすべてのプログラムに適用されます。 最適化についてよく知っている人なら誰でもこれを確認できます。



私の友人であるgzip



に捧げられたRalph Holzmannのスピーチを覚えています。 彼は、スクリプトをロードするための古いライブラリであるLABjs



で行った実験について話しました。 このパフォーマンスを見ることができます 。 私がここで話していることは、ビデオの30分から始めて約2分半かかります。



当時のLABjsでは、完成したコードのサイズを最適化することを目的とした奇妙なことが行われました。 obj.foo



は、通常のオブジェクト表記( obj.foo



)を使用する代わりに、キーを文字列に格納し、角括弧を使用してオブジェクトのコンテンツ( obj[stringForFoo]



)にアクセスしました。 これは、 gzip



コードを縮小および圧縮した後、通常とは異なる方法で作成されたコードが、通常の方法で作成されたコードよりも小さくなるはずだったためです。



Ralphはこのコードを分岐して最適化を削除し、縮小とgzip圧縮のためにコードを最適化する方法を考えずに、通常の方法で書き換えました。



「最適化」を取り除くことで、結果のファイルのサイズを5.3%削減できることがわかりました。 明らかに、ライブラリの作成者は、利点を与えるかどうかをチェックせずに、「最適化された」形式ですぐにそれを書きました。 特定の最適化によって何かが改善されるかどうかを測定することは不可能です。 さらに、最適化によって状況が悪化するだけの場合、これについてもわかりません。



時期尚早な最適化は、開発時間を大幅に増加させ、コードの純度を悪化させるだけでなく、LABjsの場合のように、マイナスの結果をもたらし、問題を引き起こす可能性があります。 ライブラリの作成者がパフォーマンスの問題を想像する代わりに測定を行った場合、開発時間を節約し、より優れた特性を備えたよりクリーンなコードをリリースします。



ここでこのツイートを引用ます。「アームチェアでくつろいでいる人が、パフォーマンスの測定をせずに問題を解決するのに時間がかかるコードがあると主張するのはいらいらします。」 私はこの観点を支持します。



だから、私は繰り返します-時期尚早な最適化を行いません。 さて、Reactに戻ります。



組み込み関数がパフォーマンスを低下させると言われるのはなぜですか?



組み込み関数は、2つの理由で遅いと見なされます。 まず、これはメモリ消費とガベージコレクションに関する懸念によるものです。 第二にshouldComponentUpdate



です。 これらの懸念を調べてみましょう。



▍メモリ消費とガベージコレクション



まず、プログラマー(およびestlint構成 )は、 組み込み関数を作成する際のメモリー消費とガベージコレクションからのシステム負荷を懸念します。 これは、JSの矢印関数がまだ広く使用されていなかった時代の遺産です。 ビルトインコンストラクトのReactコードでbind



コマンドが頻繁に使用された場合、これは歴史的にパフォーマンスの低下につながりました。 例:



 <div> {stuff.map(function(thing) {   <div>{thing.whatever}</div> }.bind(this)} </div>
      
      





Function.prototype.bind



の問題はここ修正され 、矢印関数は言語の組み込み機能として使用されるか、またはbabelを使用して通常の関数に変換されました。 そして、そういうわけで、私たちはそれらが遅いわけではないと仮定することができます。



一部のコードが遅くなるという前提を立てる必要はありません。 いつものようにコードを書き、パフォーマンスを測定します。 問題が見つかった場合は、修正してください。 矢印関数が高速に動作することを証明する必要はありません。他の人にそれらが遅いことを証明させてください。 それ以外の場合は、時期尚早な最適化です。



私の知る限り、誰もアプリケーションの研究を行っていません。組み込み機能がパフォーマンスの問題につながることを示しています。 ここまでは、これについて話す必要はありませんが、いずれにしても、ここで別のアイデアを共有します。

システムの組み込み関数の作成による負荷がこれを防ぐ特別なエスリントルールを作成するのに十分高い場合、なぜこれらの重い操作をシステムの速度に影響を与えるという点で非常に重要な初期化ユニットに移動しようとするのでしょうか?



 class Dashboard extends Component { state = { handlingThings: false } constructor(props) {   super(props)     this.handleThings = () =>     this.setState({ handlingThings: true })   this.handleStuff = () => { /* ... */ }   //       bind   this.handleMoreStuff = this.handleMoreStuff.bind(this) } handleMoreStuff() { /* ... */ } render() {   return (     <div>       {this.state.handlingThings ? (         <div>           <button onClick={this.handleStuff}/>           <button onClick={this.handleMoreStuff}/>         </div>       ) : (         <button onClick={this.handleThings}/>       )}     </div>   ) } }
      
      





予備的な最適化では、コンポーネントの初期化を3回遅くしました。 すべてのイベントハンドラーが組み込み関数であった場合、 render



の最初の呼び出しで必要な関数は1つだけです。 代わりに、3つ作成します。 さらに、パフォーマンス測定は行われなかったため、これを問題と見なす理由はありません。



ただし、必要なものと不要なものをすべて組み込み関数に転送するという考えに夢中になるべきではありません。 上記のアイデアに触発されて、誰かが初期のレンダリングを高速化するために組み込み関数の広範な使用を必要とするeslintルールを作成することに決めた場合、同じ有害な時期尚早な最適化に直面します。



urePureComponentおよびshouldComponentUpdate



問題の真の核はPureComponent



shouldComponentUpdate



ます。 パフォーマンスを最適化するためには、2つのことを理解する必要がありますshouldComponentUpdate



の機能と、JavaScriptでの厳密な同等性の比較の仕組みです。 これらの概念を理解しなくても、コードの高速化を試みても事態は悪化するだけです。



setState



setState



、Reactは古い要素と新しい要素を比較し(これを調整と呼びます)、受け取った情報を使用して実際のDOMの要素を更新します。 チェックする必要のある要素が多すぎる場合(大きなSVGのようなもの)、この操作はかなり遅いことがあります。 このような場合、ReactはshouldComponentUpdate



という回避策を提供します。



 class Avatar extends Component { shouldComponentUpdate(nextProps, nextState) {   return stuffChanged(this, nextProps, nextState)) } render() {   return //... } }
      
      





Reactが古い要素と新しい要素を比較する前に、コンポーネントshouldComponentUpdate



設定されている場合、 shouldComponentUpdate



shouldComponentUpdate



、何かが変更されたかどうかを確認します。 応答がfalse



返した場合、Reactは要素比較操作を完全にスキップするため、時間を節約できます。 コンポーネントが十分に大きい場合、これはパフォーマンスに顕著な影響をもたらす可能性があります。



コンポーネントを最適化する最も一般的な方法は、 React.PureComponent



ではなくReact.PureComponent



を拡張することReact.Component



PureComponent



PureComponent



プロパティと状態を比較します。その結果、自分で行う必要はありません。

class Avatar extends React.PureComponent { ... }







Avatar



クラスは、更新を要求する前にプロパティと状態を操作するときに、厳密な等価比較を使用するようになりました。 これにより、プログラムが高速化されることが期待できます。



厳密な平等の比較



JavaScriptには、 string



number



boolean



null



undefined



symbol



6つのプリミティブ型があります。 同じ値を格納するプリミティブ型の2つの変数の厳密な比較が実行されると、 true



取得されます。 例:



 const one = 1 const uno = 1 one === uno // true
      
      





PureComponent



プロパティを比較するとき、厳密な比較を使用します。 これは、 <Toggler isOpen={true}/>



ような埋め込みプリミティブ値に最適です。



プロパティを比較するときの問題は、他のタイプ、つまり申し訳ありません-唯一のタイプで発生します。 JSの他のすべてはObject



です。 しかし、関数と配列はどうでしょうか? 実際、これらはすべてオブジェクトです。 「関数は、実行のために呼び出される追加の機能を持つ通常のオブジェクトです」というMDNドキュメントからの抜粋を引用できます。



まあ私は何を言うことができます-JSはJSです。 いずれの場合でも、異なるオブジェクトの厳密な比較は、たとえ同じ値が含まれていても、 false



を返しfalse







 const one = { n: 1 } const uno = { n: 1 } one === uno // false one === one // true
      
      





そのため、オブジェクトをJSXコードに埋め込むと、 PureComponent



のプロパティの適切な比較が不可能になり、その結果、React要素のより時間のかかる比較が行われます。 この比較では、結果としてコンポーネントが変更されていないことがわかります-2つの比較での時間の損失。



 //   <Avatar user={{ id: 'ryan' }}/> //   <Avatar user={{ id: 'ryan' }}/> //   ,  - ,   {} !== {} //   () ,    
      
      





関数はオブジェクトであり、 PureComponent



はプロパティの等価性を厳密にチェックするため、プロパティの分析時に組み込み関数の比較は常に、それらが異なるというメッセージで終了します。その後、マッチング手順中の要素の比較への移行が実行されます。



これは組み込み関数だけに適用されるわけではないことに気付くかもしれません。 通常のオブジェクトや配列についても同じことが言えます。



shouldComponentUpdate



同一の関数を比較するときに期待することを行うには、関数の参照IDを維持する必要があります。 経験豊富なJS開発者にとって、これはそれほど悪いニュースではありません。 しかし、 Michaelと私がさまざまなレベルのトレーニングを受けた約3,500人のトレーニング後に学んだことを考慮すると、このタスクは学生にとってそれほど単純ではなかったことがわかります。 ESクラスはここでも役に立たないため、この状況では他のJS機能を使用する必要があることに注意してください。



 class Dashboard extends Component { constructor(props) {   super(props)     //  bind?    ,     20,   //  .   //  ,    .   this.handleStuff = this.handleStuff.bind(this)   // _this -   .   var _this = this   this.handleStuff = function() {     _this.setState({})   }     //     ES, , ,       //   ( ,   babel    ).   //      ,       -     //     .   this.handleStuff = () => {     this.setState({})   } } //   ,      JavaScript, //       ,    TC39  //      . handleStuff = () => {} }
      
      





関数の参照IDを保存するための手法の研究は、驚くほど長い会話につながることに注意する必要があります。 eslint構成の要件を満たしたい場合を除き、プログラマーに電話する理由はありません。 私が見せたかった主なことは、組み込み関数が最適化に干渉しないことです。 次に、パフォーマンスの最適化に関する独自のストーリーを共有します。



PureComponentの使用方法



PureRenderMixin



(これは以前のバージョンのReactのデザインで、後にPureComponent



になったデザイン)を初めて知ったとき、多くの次元を使用して、アプリケーションのパフォーマンスを評価しました。 次に、 PureRenderMixin



をすべてのコンポーネントに追加しました。 最適化されたバージョンのパフォーマンス測定を行ったとき、結果としてすべてが素晴らしくなり、誇らしげにみんなにそれを伝えることができることを望みました。



しかし、驚いたことに、アプリケーションの動作が遅くなりました。



なんで? 考えてみてください。 特定のComponent



がある場合、そのComponent



操作する際に実行する必要がある比較操作の数はいくつですか? PureComponent



どうですか? それぞれの答えは、「1つだけ」、「少なくとも1つ、時には2つ」です。 アップグレード中にコンポーネントが通常変更される場合、 PureComponent



は1つではなく2つの比較操作を実行します( shouldComponentUpdate



プロパティと状態、および通常の要素の比較)。 これは、通常、 PureComponen



tが遅くなることを意味しますが、時には速くなります。 明らかに、私のコンポーネントのほとんどは絶えず変化していたため、一般に、アプリケーションの動作が遅くなりました。 悲しいです



「生産性を高める方法」という質問に対する普遍的な答えはありません。 答えは、特定のアプリケーションのパフォーマンス測定でのみ見つけることができます。



3つの組み込み関数の使用シナリオについて



素材の冒頭で、3種類の組み込み関数を示しました。 いくつかのベースが準備されたので、それぞれについて話しましょう。 ただし、このメカニズムを使用する利点を評価するために、測定するまでPureComponent



を保持することをお勧めします。



▍DOMコンポーネントイベントハンドラー



 <button onClick={() => this.setState(…)} >click</button>
      
      





通常、ボタン、入力フィールド、またはsetState



呼び出し以外の他のDOMコンポーネントのイベントハンドラー内では何も行われません。 これにより、通常、インライン関数が最もクリーンなアプローチになります。 イベントハンドラの検索でファイルをジャンプする代わりに、それらは要素記述コードで見つけることができます。 通常、Reactコミュニティはこれを歓迎します。



button



コンポーネント(および他のDOMコンポーネント)をPureComponent



にすることさえできないため、 shouldComponentUpdate



および参照IDについて心配する必要はありません。



結果として、関数の単純な定義がシステムのかなり大きな負荷であり、心配する価値があることに同意する場合にのみ、これは遅いと考えることができます。 これがそうであるという証拠はありません。 組み込みのイベントハンドラーの不当な廃棄は、よく知られている時期尚早な最適化です。



▍「カスタムイベント」または「アクション」



 <Sidebar onToggle={(isOpen) => { this.setState({ sidebarIsOpen: isOpen }) }}/>
      
      





Sidebar —



PureComponent



の場合、プロパティの比較は渡しません。 繰り返しになりますが、ハンドラーは単純であるため、それを埋め込むことが最善の解決策になる可能性があります。



次に、 onToggle



などのイベントと、 Sidebar



それらを比較する理由について説明します。 shouldComponentUpdate



プロパティの違いを探す理由は2つしかありません。



  1. プロパティはレンダリングに使用されます。

  2. このプロパティは、 componentWillReceiveProps



    componentDidUpdate



    、またはcomponentWillReceiveProps



    で副作用を達成するために使用されcomponentWillUpdate







ほとんどのon<whatever>



プロパティはこれらの要件を満たしていません。 したがって、ほとんどのPureComponent



使用PureComponent



、不必要な比較が行われ、開発者は参照IDハンドラーを不必要に維持する必要があります。



変更できるプロパティのみを比較する必要があります。 したがって、ハンドラーは要素記述コードに含めることができ、これはすべて迅速に機能します。パフォーマンスが懸念される場合、このアプローチでは比較が少なくて済むことに注意してください。



ほとんどのコンポーネントについては、 PureComponentMinusHandlers



から継承するのではなく、 PureComponentMinusHandlers



クラスを作成して継承することをおPureComponent



ます。 これは、すべての機能チェックをスキップするのに役立ちます。 必要なものだけ。 まあ-ほとんど必要なもの。



関数を取得し、その関数を別のコンポーネントに直接渡す場合、廃止されます。 これを見てください:



 // 1.    . // 2.     , //   ,   . // 3.    setState     // **  . // 4.     ,  //  . // 5.        //   ,     //   . class App extends React.Component { state = { val: "one" } componentDidMount() {   this.setState({ val: "two" }) } render() {   return <Form value={this.state.val} /> } } const Form = props => ( <Button   onClick={() => {     submit(props.value)   }} /> ) class Button extends React.Component { shouldComponentUpdate() {   // ,    ,  .   return false } handleClick = () => this.props.onClick() render() {   return (     <div>       <button onClick={this.props.onClick}>This one is stale</button>       <button onClick={() => this.props.onClick()}>This one works</button>       <button onClick={this.handleClick}>This one works too</button>     </div>   ) } }
      
      





ここで 、このコードを試すことができます。



したがって、 PureRenderWithoutHandlers



から継承するというアイデアが好きな場合は、比較に関係しないハンドラーを他のコンポーネントに直接渡さないでください。何らかの方法でそれらをラップする必要があります。



ここで、参照IDを維持するか、参照IDを回避する必要があります! パフォーマンスの最適化へようこそ。 少なくとも、このアプローチでは、最適化されたコンポーネントに負担がかかり、それを使用するコードには負担がかかりません。



このサンプルアプリケーションは、 Andrew Clarkによって公開された後に作成した資料に追加されたものであると正直に言わなければなりません。 したがって、参照整合性を維持するタイミングと維持しないタイミングを正確に知っているように思えるかもしれません。



▍レンダリングプロパティ



 <Route path="/topic/:id" render={({ match }) => (   <div>     <h1>{match.params.id}</h1>}   </div> ) />
      
      





render —



プロパティrender —



、共有状態を作成および維持するために存在するコンポーネントを作成するために使用されるテンプレートですここで詳細を確認できます )。 render



プロパティの内容は、コンポーネントにrender



。 例:



 const App = (props) => ( <div>   <h1>Welcome, {props.name}</h1>   <Route path="/" render={() => (     <div>       {/*         props.name    Route              ,  Route            PureComponent,              ,     .       */}       <h1>Hey, {props.name}, let's get started!</h1>     </div> )}/> </div> )
      
      





, render



shouldComponentUpdate



. , PureComponent



.



, , render



. — , .



まとめ



  1. , , .

  2. , . , .

  3. PureComponent



    shouldComponentUpdate



    , , ( ).



, , , . , , , .



親愛なる読者! React-?



All Articles