私が 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 に内容を入力するたびに、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>;
});