この記事では、
- コンポーネントが再レンダリングされるのはどのようなときか
- 不要な再レンダリングによるパフォーマンス悪化を防ぎ、再レンダリングを最適化する方法
についてまとめます。
その前に、レンダリングとは
stateやpropsの変更差分を反映したDOMを再構築すること。
レンダリングが起きると、コンポーネントの頭から処理が順に行われる。
ちなみに、再構築されたDOMをブラウザに反映させることは描画という。
再レンダリングされる状況3つ
- stateが更新されたコンポーネントは再レンダリングされる
- propsが変更されたコンポーネントは再レンダリングされる
- 親コンポーネントが再レンダリングされると、子要素も再レンダリングされる
①stateが更新されたとき
src/App.jsx
import React, { useState } from "react";
const App = () => {
console.log("App");
const [count, setCount] = useState(0)
const disabled = count<=0
const increment = () =>setCount(count + 1)
const decrement = () =>setCount(count - 1)
return (
<div style={{ margin: "50px" }}>
<h3>りんごの在庫:{count}個</h3>
<button onClick={increment}>1つ購入</button>
<button disabled={disabled} onClick={decrement}>1つ食べる</button>
{count <= 0 &&
<h4 style={{ color: "red" }}>
りんごを食べ切ったよ!!
</h4>
}
</div>
);
}
export default App;
最初に画面が描画される時、またボタンを押す度に、コンソール に「App」と表示される。
②propsが更新されたとき
親コンポーネントにComponentという子コンポーネントをimportし、親コンポーネントからComponentコンポーネントへcountというpropsを渡す。
src/App.jsx
import React, { useState } from "react"
import { Component } from "./components/Component"
const App = () => {
console.log("App");
const [count, setCount] = useState(0)
const disabled = count<=0
const increment = () =>setCount(count + 1)
const decrement = () =>setCount(count - 1)
return (
<div style={{ margin: "50px" }}>
<h3>りんごの在庫:{count}個</h3>
<button onClick={increment}>1つ購入</button>
<button disabled={disabled} onClick={decrement}>1つ食べる</button>
{count <= 0 &&
<h4 style={{ color: "red" }}>
りんごを食べ切ったよ!!
</h4>
}
<Component count={count} />
</div>
);
}
export default App;
子コンポーネントでpropsを受け取る。
sec/coponents/Component.jsx
export const Component = (props) => {
const { count } = props;
console.log("Component");
return <p>Component{count}</p>;
};
最初に画面が描画されるときと、ボタンを押す度に、コンソール には「App」と「Component」の両方が表示される。
③親コンポーネントが再レンダリングされたとき
親コンポーネントにSecondComponentをimportする。
src/App.jsx
import React, { useState } from "react"
import { Component } from "./components/Component"
import { SecondComponent } from "./components/SecondComponent"
const App = () => {
console.log("App");
const [count, setCount] = useState(0)
const disabled = count<=0
const increment = () =>setCount(count + 1)
const decrement = () =>setCount(count - 1)
return (
<div style={{ margin: "50px" }}>
<h3>りんごの在庫:{count}個</h3>
<button onClick={increment}>1つ購入</button>
<button disabled={disabled} onClick={decrement}>1つ食べる</button>
{count <= 0 &&
<h4 style={{ color: "red" }}>
りんごを食べ切ったよ!!
</h4>
}
<Component count={count} />
<SecondComponent />
</div>
);
}
export default App;
propsの受け渡しの無い子コンポーネントであるSecondComponentを作成する。
src/components/SecondComponent.jsx
export const SecondComponent = () => {
console.log("SecondComponent");
return <p>SecondComponent</p>;
};
最初に画面が描画されるときと、ボタンを押す度に、コンソール には「App」「Component」「SecondComponent」の3つともが表示される。
このように必要のない再レンダリングはパフォーマンスを下げてしまうので、Reactでは再レンダリングを最適化する方法が用意されている。
再レンダリングを最適化する方法3つ
- memo
- useCallback
- useMemo
メモ化とは
メモ化とは、関数の計算結果を保持しておき、その関数の呼び出し毎の再計算を防ぐテクニック。
React固有のものではなく様々なプログラムで使われるテクニック。
Reactではメモ化を簡単に使えるようにメモ化関数が用意されている。
①memo
memoはコンポーネント(コンポーネントのレンダリング結果)をメモ化するReactのAPI(メソッド)。
memoの使い方
子コンポーネントの処理の中身を丸ごと、memoの引数に渡す。
src/components/SecondComponent.jsx
import { memo } from "react";
export const SecondComponent = memo(() => {
console.log("SecondComponent");
return <p>SecondComponent</p>;
});
このようにすることで、最初に画面が描画されるときにはコンソール に「App」「Component」「SecondComponent」の3つともが表示されるが、ボタンを押して親コンポーネントが再レンダリングされた時には「SecondComponent」はコンソールに表示されなくなる。
②useCallback
useCallbackは、メモ化されたコールバック関数を返すフック。
どんな時に必要なのか?
①のように子コンポーネントをmemo化しても、再レンダリングが発生するケースがある。
以下のコードのように、App.jsで新しいappleという関数を定義し、propsとしてSecondComponentへ受け渡してみる。
src/App.jsx
import React, { useState, useCallback } from "react";
import { Component } from "./components/Component";
import { SecondComponent } from "./components/SecondComponent";
const App = () => {
console.log("App");
const [count, setCount] = useState(0);
const disabled = count <= 0;
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const apple = () => console.log("りんご!"); //追記
return (
<div style={{ margin: "50px" }}>
<h3>りんごの在庫:{count}個</h3>
<button onClick={increment}>1つ購入</button>
<button disabled={disabled} onClick={decrement}>
1つ食べる
</button>
{count <= 0 && <h4 style={{ color: "red" }}>りんごを食べ切ったよ!!</h4>}
<Component count={count} />
<SecondComponent apple={apple} /> //propsを追記
</div>
);
};
export default App;
子コンポーネントであるSecondComponentにおいてappleを呼び出す。
src/components/SecondComponent.jsx
import { memo } from "react";
export const SecondComponent = memo((props) => {
const { apple } = props;
console.log("SecondComponent");
return (
<>
<p> SecondComponent</p>
<button onClick={apple}>りんご</button>
</>
);
});
こうすると、SecondComponentをmemo化しているにも関わらず、「1つ購入」ボタンや「1つ食べる」ボタンを押した時に、コンソール に「App」「Component」「SecondComponent」の3つともが表示されるようになってしまった。
この原因は、親コンポーネントで定義したアロー関数をpropsとして子コンポーネントに渡すと、関数の内容は変わっていなくても、親コンポーネントが再レンダリングされる度に「新しい関数が生成されpropsとして渡されている」と判断されてしまうことにある。
このような再レンダリングを防ぐために、useCallbackを使って関数をメモ化する。
useCallbackの使い方
useCallbackは、第1引数にアロー関数、第2引数に依存配列を受ける。(useEffect同様)
第2引数の指定は任意で、指定された場合は依存配列に入れた値のいずれかが変化した場合のみ第1引数の関数が再計算される。
//構文
const propsとして受け渡す関数名 = useCallback(() => memo化する関数, [依存配列]);
先ほどのappleにuseCallbackを適用してみる。
src/App.js
import React, { useState, useCallback } from "react";
import { Component } from "./components/Component";
import { SecondComponent } from "./components/SecondComponent";
const App = () => {
console.log("App");
const [count, setCount] = useState(0);
const disabled = count <= 0;
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const apple = useCallback(() => console.log("りんご!"), []);
return (
<div style={{ margin: "50px" }}>
<h3>りんごの在庫:{count}個</h3>
<button onClick={increment}>1つ購入</button>
<button disabled={disabled} onClick={decrement}>
1つ食べる
</button>
{count <= 0 && <h4 style={{ color: "red" }}>りんごを食べ切ったよ!!</h4>}
<Component count={count} />
<SecondComponent apple={apple} />
</div>
);
};
export default App;
これにより、「1つ購入」「1つ食べる」ボタンを押しても、SecondComponentは再レンダリングされないようになった。
上記では第2引数の配列を空にしているが、例えば以下のように第2引数にcountを渡すと、countが変化する度に(ここでは「1つ購入」「1つ食べる」ボタンを押す度に)SecondComponentは再レンダリングされるようになる。
const apple = useCallback(() => console.log("りんご!"), [count]);
③useMemo
useMemoは関数の結果をメモ化するフック。
どんな時に必要なのか?
処理が複雑な計算処理を定義した関数がある場合、レンダリングの度に再計算されるとパフォーマンス悪化に繋がるケースがある。
以下のように、calcSumという関数を定義し、JSX記法の中で呼び出してみる。
src/App.js
import React, { useState, useCallback } from "react";
import { Component } from "./components/Component";
import { SecondComponent } from "./components/SecondComponent";
const App = () => {
console.log("App");
const [count, setCount] = useState(0);
const disabled = count <= 0;
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const apple = useCallback(() => console.log("りんご!"), [count]);
const calcSum = () => {
console.log("calcSum");
return 1 + 2 + 3;
}; //追記
return (
<div style={{ margin: "50px" }}>
<h3>りんごの在庫:{count}個</h3>
<button onClick={increment}>1つ購入</button>
<button disabled={disabled} onClick={decrement}>
1つ食べる
</button>
{count <= 0 && <h4 style={{ color: "red" }}>りんごを食べ切ったよ!!</h4>}
{calcSum()} //追記
</div>
);
};
export default App;
すると、「1つ購入」「1つ食べる」ボタンを押して再レンダリングが起きる度に、コンソール には「App」と「calcSum」が出力され、毎回calcSumの計算処理が行われていることがわかる。
上記のcalcSumで行っているのはごく簡単な計算処理だが、これがもし複雑な計算処理であれば、毎回再計算を行うとパフォーマンス悪化につながるので、この計算結果自体をuseMemoを使ってメモ化する。
useMemoの使い方
useMemoは、第1引数にアロー関数、第2引数に依存配列を受ける。(useEffectやuseCallback同様)
第2引数の指定は任意で、指定された場合は依存配列に入れた値のいずれかが変化した場合のみ第1引数の関数が再計算される。
//構文
const propsとして受け渡す関数名 = useMemo(() => 結果を算出するロジック, [依存配列]);
先ほどのcalcSumにuseMemoを適用してみる。
src/App.js
import React, { useState, useCallback, useMemo } from "react";
import { Component } from "./components/Component";
import { SecondComponent } from "./components/SecondComponent";
const App = () => {
console.log("App");
const [count, setCount] = useState(0);
const disabled = count <= 0;
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const apple = useCallback(() => console.log("りんご!"), [count]);
const calcSum = useMemo(() => {
console.log("calcSum");
return 1 + 2 + 3;
}, []);
return (
<div style={{ margin: "50px" }}>
<h3>りんごの在庫:{count}個</h3>
<button onClick={increment}>1つ購入</button>
<button disabled={disabled} onClick={decrement}>
1つ食べる
</button>
{count <= 0 && <h4 style={{ color: "red" }}>りんごを食べ切ったよ!!</h4>}
{calcSum}
</div>
);
};
export default App;
useMemoによりcalcSumの計算結果自体がメモ化されて、calcSumは関数でなくなるので、JSX記法内で呼び出している箇所も{calcSum()}から{calcSum}へ変更する必要がある。
これにより、「1つ購入」「1つ食べる」ボタンを押しても、コンソール に「calcSum」は表示されなくなり、計算処理は最初の画面描画時に一度行われるのみとなった。
参考記事
具体例で理解するuseMemoとuseCallbackの使い方。Reactパフォーマンスチューニング | Enjoy IT Life
[【React Hooks】useCallBackの使い所を理解しよう]
(https://tyotto-good.com/blog/usecallback)