suemor

suemor

前端萌新
telegram
github
twitter

React の memo、useMemo および useCallback

私が React に触れてから数ヶ月が経ちましたが、その間、重複レンダリングを避ける方法について悩んでいました。そこで、今日はこのトピックについて話しましょう。

パフォーマンス最適化の方法を説明する前に、React がなぜ再レンダリングするのかについて話しましょう。

React が再レンダリングする理由#

状態の変化は、React ツリー内部で更新が発生する唯一の理由の一つです

import { useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  console.log('render');
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
};

input 例子

明らかに、input に内容を入力するたびに、console.log('render')が出力されます。これは、colorの状態が変化したためであり、props とは完全に無関係であることを間接的に示しています

このようなオーバーヘッドは非常に非合理的であり、次にそれを最適化します。

パフォーマンス最適化#

方法 1: State の抽出#

私たちは React が単方向データフローであることを知っているので、State を抽出するだけで済みます。

import { useState } from "react";

const App = () => {
  return (
    <div>
      <Input />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  console.log("render");
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
};

const Input = () => {
  let [color, setColor] = useState("red");

  return <input value={color} onChange={(e) => setColor(e.target.value)} />;
}

方法 2: memo#

React.memo は高階コンポーネントであり、それによってラップされたコンポーネントを純粋なコンポーネントにすることができます。つまり、props が変わらない限り、React はそれを更新しません。

import { memo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = memo(() => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
})

方法 3: react children#

App が状態を変更しないため、ExpensiveTree は再レンダリングを回避しました。

import { FC, PropsWithChildren, useState } from "react";

const App = () => {
  return (
    <ColorWrapper>
      <ExpensiveTree />
    </ColorWrapper>
  );
};

const ColorWrapper: FC<PropsWithChildren> = ({ children }) => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
};

useMemo と useCallback の使用#

useMemo#

useMemo は Vue のComputedに似ており、依存関係が変わったときにのみ新しい値を再計算します。

これにより、input が変更されると dirtyWork が繰り返し実行されることはありません。

import { useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const [number,setNumber] = useState(0)
  
  const dirtyWork = useMemo(() => {
    console.log('大量の作業を行っています');
    return number
  },[number])
  
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <h1>{dirtyWork}</h1>
    </div>
  );
};

また、前のセクションの例も useMemo を使用して修正できます。

import { memo, useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {useMemo(
        () => (
          <ExpensiveTree />
        ),
        []
      )}
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
};

useCallback#

次の例を見てみましょう。

import { FC, memo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = ()=> {
    console.log('ハハハ');
  }
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn}/>
    </div>
  );
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  console.log('render'); // 依然として更新され続けます
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
})

ExpensiveTree が memo でラップされていても、input に内容を入力すると ExpensiveTree が更新され続けることがわかります。この場合、親コンポーネントの fn 関数を useCallback でラップするだけで済みます。

したがって、useCallback は通常、関数を子コンポーネントに渡す必要がある場合に使用されます。useCallback を使用して上記の例を書き換えます。

import { FC, memo, useCallback, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = useCallback(()=> {
    console.log('ハハハ');
  },[])
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn}/>
    </div>
  );
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  console.log('render');
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
})

useCallback は実際には useMemo の構文糖であることに気付くかもしれません。上記の例でも useMemo を使用して書き換えることができます。

import { FC, memo, useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = useMemo(() => {
    return () => console.log("ハハハ");
  }, []);
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn} />
    </div>
  );
};

const ExpensiveTree: FC<{ fn: () => void }> = memo(({ fn }) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 遅延
  }
  console.log("render");
  return <p>私は非常に遅いコンポーネントツリーです。</p>;
});

参考資料#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。