web-dev-qa-db-ja.com

反応フックを使用するすべてのレンダリングでハンドラーを作成することによるパフォーマンスの低下

現在、新しいreact hooks APIの使用例と、それを使用して何ができるかについて非常に驚いています。

実験中に出てきた質問は、useCallbackを使用するときに単に破棄するだけの新しいハンドラー関数を常に作成するのはどれだけ費用がかかるかということでした。

この例を考えてみましょう:

const MyCounter = ({initial}) => {
    const [count, setCount] = useState(initial);

    const increase = useCallback(() => setCount(count => count + 1), [setCount]);
    const decrease = useCallback(() => setCount(count => count > 0 ? count - 1 : 0), [setCount]);

    return (
        <div className="counter">
            <p>The count is {count}.</p>
            <button onClick={decrease} disabled={count === 0}> - </button>
            <button onClick={increase}> + </button>
        </div>
    );
};

ハンドラーをuseCallbackにラップして、新しいハンドラーを渡すたびに渡すことを避けていますが、インライン矢印関数は、ほとんどの場合破棄するために作成する必要があります。

いくつかのコンポーネントのみをレンダリングする場合、おそらく大した問題ではありません。しかし、それを何千回も行うと、パフォーマンスへの影響はどれほど大きくなるのでしょうか?顕著なパフォーマンスペナルティはありますか?そして、それを避ける方法は何でしょうか?おそらく、新しいハンドラを作成する必要がある場合にのみ呼び出される静的ハンドラファクトリですか?

16
trixn

React FAQに説明があります

renderで関数を作成するためにフックが遅くなりますか?

いいえ。最新のブラウザでは、クラスと比較したクロージャーの生のパフォーマンスは、極端なシナリオを除いて大きく変わりません。

さらに、フックの設計がいくつかの方法でより効率的であることを考慮してください。

フックは、クラスインスタンスを作成し、コンストラクターでイベントハンドラーをバインドするコストなど、クラスに必要な多くのオーバーヘッドを回避します。

フックを使用した慣用的なコードでは、高次コンポーネントを使用するコードベース、レンダリングプロップ、およびコンテキストで一般的な深いコンポーネントツリーのネストは必要ありません。コンポーネントツリーが小さい場合、Reactの作業は少なくなります。

伝統的に、React=のインライン関数に関するパフォーマンスの問題は、各レンダリングで新しいコールバックを渡すことで子コンポーネントのshouldComponentUpdate最適化が中断されることに関連していました。

したがって、フックが提供する全体的な利点は、新しい関数を作成することのペナルティよりもはるかに大きいです。

さらに、機能コンポーネントについては、useMemoを使用して最適化することにより、小道具に変更がない場合にコンポーネントが再レンダリングされるようにすることができます。

6
Shubham Khatri

ただし、1000回実行した場合のパフォーマンスへの影響はどれくらいですか?顕著なパフォーマンスの低下はありますか?

アプリによって異なります。単に1000行のカウンターをレンダリングするだけの場合は、以下のコードスニペットでわかるように、おそらく大丈夫です。個々の<Counter />、そのカウンターのみが再レンダリングされ、他の999カウンターは影響を受けません。

しかし、あなたはここで無関係なことを心配していると思います。実際のアプリでは、1000個のリスト要素がレンダリングされることはほとんどありません。アプリで1000個のアイテムをレンダリングする必要がある場合、アプリの設計方法に何らかの問題がある可能性があります。

  1. DOMで1000個のアイテムをレンダリングしないでください。これは通常、最新のJavaScriptフレームワークの有無にかかわらず、パフォーマンスとUXの観点からは悪いことです。ウィンドウ手法を使用して、画面に表示されるアイテムのみをレンダリングできます。その他のオフスクリーンアイテムはメモリに格納できます。

  2. shouldComponentUpdate(またはuseMemo)を実装して、トップレベルのコンポーネントが再レンダリングする必要がある場合に他のアイテムが再レンダリングされないようにします。

  3. 関数を使用することで、Reactが自動的に処理してくれるので、知らない内部で発生するクラスやその他のクラス関連のもののオーバーヘッドを回避できます。関数でいくつかのフックを呼び出すためパフォーマンスが向上しますが、他の場所でもパフォーマンスが向上します。

  4. 最後に、useXXXフックを呼び出しており、フックに渡したコールバック関数を実行していないことに注意してください。 Reactチームは、フック呼び出しを軽量な呼び出しフックにするのに良い仕事をしたと思います。

そしてそれを避ける方法は何でしょうか?

ステートフルアイテムを何千回も作成する必要がある現実のシナリオがあるとは思いません。しかし、本当に必要な場合は、状態を親コンポーネントに上げて、値とインクリメント/デクリメントコールバックを小道具として各アイテムに渡す方が良いでしょう。そうすれば、個々のアイテムは状態修飾子のコールバックを作成する必要がなく、親からのコールバックプロップを使用するだけです。また、ステートレスな子コンポーネントにより、さまざまな既知のパフォーマンス最適化の実装が容易になります。

最後に、この問題を心配する必要はないことを繰り返したいと思います。それは、このような状況に対処するのではなく、そのような状況に陥ることを避け、ウィンドウイングやページネーションなどのテクニックを活用して、現在のページに表示する必要があります。

const Counter = ({ initial }) => {
  const [count, setCount] = React.useState(initial);

  const increase = React.useCallback(() => setCount(count => count + 1), [setCount]);
  const decrease = React.useCallback(
    () => setCount(count => (count > 0 ? count - 1 : 0)),
    [setCount]
  );

  return (
    <div className="counter">
      <p>The count is {count}.</p>
      <button onClick={decrease} disabled={count === 0}>
        -
      </button>
      <button onClick={increase}>+</button>
    </div>
  );
};

function App() {
  const [count, setCount] = React.useState(1000);
  return (
    <div>
      <h1>Counters: {count}</h1>
      <button onClick={() => {
        setCount(count + 1);
      }}>Add Counter</button>
      <hr/>
      {(() => {
        const items = [];
        for (let i = 0; i < count; i++) {
          items.Push(<Counter key={i} initial={i} />);
        }
        return items;
      })()}
    </div>
  );
}


ReactDOM.render(
  <div>
    <App />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
2
Yangshun Tay

あなたは正しい、大規模なアプリケーションでは、これはパフォーマンスの問題につながる可能性があります。コンポーネントに渡す前にハンドラーをバインドすると、子コンポーネントが余分な再レンダリングを実行する可能性がなくなります。

<button onClick={(e) => this.handleClick(e)}>click me!</button>
<button onClick={this.handleClick.bind(this)}>click me!</button>

両方とも同等です。 Reactイベントを表すe引数は、アロー関数では明示的に渡す必要があり、バインドでは引数は自動的に転送されます。

0