Lesson 6
再レンダリング時に不要な処理を実行しない方法
Lesson 6
Chapter 1
再レンダリングが起きる条件
これまでにProps、ルーティング、State等を学習してきました。レッスン5ではレンダリング後の処理の制御手法としてuseEffectを学びました。本レッスンではレンダリングの最適化手法について学習していきます。
再レンダリングが起きる三条件
Reactで表示速度を最適化するためには、コンポーネントの再レンダリングの条件を把握し、不必要なレンダリングを抑制するコードを記述していくことが重要です。まず最初に、コンポーネントの再レンダリングが起きる条件を学習していきます。コンポーネントの再レンダリングが起こる条件には以下の3つがあります。
- コンポーネントのStateが更新された時
- コンポーネントのPropsが更新された時
- 親コンポーネントがレンダリングされた時
それぞれの条件で実際に再レンダリングされることを簡単なコードを作成して確認してみます。新たにChild.jsxとGrandChild.jsxというコンポーネントファイルを作成しますので、"src/components/"に保存してください。
コンポーネントのStateが更新された時
以下に示すChildコンポーネントのStateが更新されたときに、Childコンポーネントが再レンダリングされることを確認します。
Child.jsx
import { useState } from "react";
const Child = () => {
const [num, setNum] = useState(0);
console.log("Childをレンダリングしました。");
const onClickCountUp = () => {
setNum(num+1);
}
return(
<div style = {{ border : "2px solid #0080ff", width: "90%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#0080ff", padding: "3px 10px", color: "#fff"}}>Child</span></div>
lt;p>{`Child's state=${num}`}</p>
<button onClick = {onClickCountUp} >count up Child's state</button>
</div>
);
};
export { Child };
Child.jsxにおいて、コンポーネント内部の以下の記述が重要です。
const [num, setNum] = useState(0);
console.log("Childをレンダリングしました。");
numというStateを定義しています。numは初期値0の数値を扱うStateです。このStateが更新されてコンポーネントが再レンダリングされれば、その下の"console.log("Childをレンダリングしました。");"が実行されます。ここではStateの更新により再レンダリングされることを確認するのが目的なので、これ以上コードの詳細には立ち入らないことにします。続いてChild.jsxの親要素となるApp.jsxを以下のように編集しましょう。
App.jsx
import { Child } from "./components/Child";
const App = () => {
console.log("Appをレンダリングしました。");
return(
<div style = {{ border : "2px solid #FF0000 ", width: "95%"}} >
<div><span style={{background: "#FF0000 ", padding: "3px 10px", color: "#fff"}}>App</span></div>
<Child/>
</div>
);
};
export {App};
App.jsxについても要点のみ説明します。最初に、componentsファルダからChildをimportしています。App関数内部にはレンダリングの状態が確認できるよう"console.log("Appをレンダリングしました。");"という一文が記述されています。return文を見ると、ChildコンポーネントがAppコンポーネントの内部に表示されるように記述されています。確認出来たら、ブラウザで表示を見てみましょう。最初に示す画像がプログラム実行時のブラウザの表示になります。
Appコンポーネント内にChildコンポーネントの内容が表示されています。
Appコンポーネントを示す赤色の枠内にChildコンポーネントを示す青色の枠が表示されています。Childコンポーネントの"count up Child's state"のボタンはnumに1追加するボタンになります。コンソール表示からApp、Childコンポーネントについて初回のレンダリングが実行されていることを確認できます。この状態から、"count up Child's state"をクリックし、numを更新した後の表示が以下になります。
コンソールに"Childをレンダリングしました。"の表示が追加されました。
コンソールに"Childをレンダリングしました。"の表示が追加されたのが確認できます。ChildコンポーネントのStateが更新され再レンダリングされていることが分かります。
コンポーネントのPropsが更新された時
次に、以下に示すChildコンポーネントからその次に示すGrandChildコンポーネントへのPropsが更新されたときに、GrandChildコンポーネントが再レンダリングされることを確認します。
Child.jsx
import { useState } from "react";
import { GrandChild } from "./GrandChild"
const Child = () => {
const [num, setNum] = useState(0);
console.log("Childをレンダリングしました。");
const onClickCountUp = () => {
setNum(num+1);
}
return(
<div style = {{ border : "2px solid #0080ff", width: "90%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#0080ff", padding: "3px 10px", color: "#fff"}}>Child</span></div>
<p>{`Child's state=${num}`}</p>
<button onClick = {onClickCountUp} >count up Child's state</button>
<GrandChild stateOfChild = {num} />
</div>
);
};
export { Child };
GrandChild.jsx
const GrandChild = ({stateOfChild}) => {
console.log("GrandChildをレンダリングしました。")
return(
<div style = {{ border : "2px solid #ff8000", width: "80%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#ff8000", padding: "3px 10px", color: "#fff"}}>GrandChild</span></div>
<p>{`GrandChild's props=${stateOfChild}`}</p>
</div>
);
};
export {GrandChild};
最初にChildコンポーネントを見てみましょう。前回からの違いを確認すると、GrandChildコンポーネントがimportされ、GrandChildコンポーネントを表示する記述が追加されています。またChildコンポーネントのStateであるnumを"stateOfChild"という名前のPropsでGrandChildコンポーネントに渡していることが確認できます。続いてGrandChildコンポーネントを確認してみましょう。他のコンポーネント同様に再レンダリングの確認用として、関数の最初に"console.log("GrandChildをレンダリングしました。");"が記述されています。ブラウザで表示を見てみましょう。最初に示す画像がプログラム実行時のブラウザの表示になります。
Childコンポーネント内にGrandChildコンポーネントの内容が表示されています。
Childコンポーネントを示す青色の枠内にGrandChildコンポーネントを示す橙色の枠が表示されています。コンソール表示からApp、Child、GrandChildコンポーネント全てについて初回のレンダリングが実行されていることを確認できます。この状態から、"count up Child's state"をクリックし、ChildコンポーネントのStateを更新すると、Propsの中身も更新されます。その状態が以下になります。
コンソールに"Childをレンダリングしました。"と"GrandChildをレンダリングしました。"の表示が追加されました。
コンソールに"Childをレンダリングしました。"と"GrandChildをレンダリングしました。"の表示が追加されたのが確認できます。GrandChildコンポーネントへ渡されたPropsが更新され再レンダリングされていることが分かります。
親コンポーネントがレンダリングされた時
最後に、以下に示すAppコンポーネントが更新されたときに、ChildおよびGrandChildコンポーネントが再レンダリングされることを確認します。
App.jsx
import { useState } from "react";
import { Child } from "./components/Child";
const App = () => {
const [state, setState] = useState(false);
console.log("Appをレンダリングしました。");
const onClickButton = () => {
setState(!state);
}
return(
<div style = {{ border : "2px solid #FF0000 ", width: "95%"}} >
<div><span style={{background: "#FF0000 ", padding: "3px 10px", color: "#fff"}}>App</span></div>
<p>{`App's state=${state}`}</p>
<button onClick = {onClickButton} >update App's state</button>
<Child/>
</div>
);
};
export {App};
Appコンポーネントを確認してみましょう。前回との違いは、stateというStateを定義していること、return文にてstateの状態を表示する"App's state= . . . "というメッセージとstateの状態を変更する”update App's state”というボタンを追加した部分になります。stateの初期値はfalseでボタンのクリックにより、状態が反転します。ブラウザで表示を見てみましょう。最初に示す画像がプログラム実行時のブラウザの表示になります。
Appコンポーネント内にメッセージとボタンが追加されています。
Appコンポーネントの"update App's state"ボタンは真偽値であるstateを反転するボタンになります。コンソール表示からApp、Child、GrandChildコンポーネント全てについて初回のレンダリングが実行されていることを確認できます。この状態から、"update App's state"ボタンを1回クリックし、AppコンポーネントのStateを更新した状態が以下になります。
コンソールに"Appをレンダリングしました。"、"Childをレンダリングしました。"、"GrandChildをレンダリングしました。"の表示が追加されました。
コンソールに"Appをレンダリングしました。"、"Childをレンダリングしました。"、"GrandChildをレンダリングしました。"の表示が追加されたのが確認できます。AppコンポーネントのStateが更新され再レンダリングされると共に、子コンポーネントであるChildコンポーネントも再レンダリングされていることが分かります。更に、Childコンポーネントの再レンダリングに伴いその子コンポーネントであるGrandChildコンポーネントも再レンダリングされたことが分かります。

Lesson 6
Chapter 2
レンダリングの最適化方法
前のチャプターにおいて、コンポーネントが再レンダリングされる3つの条件について詳しく見てきました。本チャプターではこれらを最適化する方法について説明していきます。
メモ化とは
Reactで再レンダリングの制御のために、コンポーネント、関数あるいは変数の前回の処理結果を保持しておくことをメモ化といいます。メモ化によって不必要な計算を省くことによって、処理を高速化することができます。
コンポーネントをメモ化する(memo)
最初に、コンポーネントをメモ化する方法を説明します。コンポーネントは以下の書き方でメモ化することができます。
const Component = memo(() => {});
ここで、"Component"がコンポーネント名であり、memo( . . . )の . . . の部分が処理内容の定義になります。コンポーネントのメモ化により、Propsに変化がない場合に再レンダリングされなくなります。チャプター1の最後に使用したChildコンポーネントを以下のようにメモ化して、Appコンポーネントの更新に伴う変化について確認してみます。
Child.jsx
import { useState, memo } from "react";
import { GrandChild } from "./GrandChild"
const Child = memo(() => {
const [num, setNum] = useState(0);
console.log("Childをレンダリングしました。");
const onClickCountUp = () => {
setNum(num+1);
}
return(
<div style = {{ border : "2px solid #0080ff", width: "90%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#0080ff", padding: "3px 10px", color: "#fff"}}>Child</span></div>
<p>{`Child's state=${num}`}</p>
<button onClick = {onClickCountUp} >count up Child's state</button>
<GrandChild stateOfChild = {num} />
</div>
);
});
export { Child };
前回からの変更点は単純に"const Child = memo(() => { . . . });"とコンポーネントの関数定義部を"memo"で囲っただけです。"memo"はReactが提供する機能ですので、importを忘れないようにしてください。ブラウザで確認してみましょう。プログラムを実行開始時の表示は前に示したのと変わらないので、ここでは"update App's state"ボタンをクリックした後の表示を確認してみます。以下の画像の表示になります。
コンソールに"Appをレンダリングしました。"の表示が追加されました。
コンソールには"Appをレンダリングしました。"の表示のみが追加されています。AppコンポーネントはStateを更新しているので再レンダリングされていますが、メモ化したChildコンポーネント以下は再レンダリングされていないことが確認できます。
関数をメモ化する(useCallback)
次に、関数のメモ化について説明します。まず以下のようにChildコンポーネントとGrandChildコンポーネントを編集した時のコンポーネントのレンダリングについて確認します。
Child.jsx
import { useState, memo } from "react";
import { GrandChild } from "./GrandChild"
const Child = memo(() => {
const [num, setNum] = useState(0);
console.log("Childをレンダリングしました。");
const onClickCountUp = () => {
setNum(num+1);
}
return(
<div style = {{ border : "2px solid #0080ff", width: "90%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#0080ff", padding: "3px 10px", color: "#fff"}}>Child</span></div>
<p>{`Child's state=${num}`}</p>
{/*<button onClick = {onClickCountUp} >count up Child's state</button> を削除*/ }
<GrandChild onClickCountUp = {onClickCountUp} /> {/* stateOfChild = {num} を onClickCountUp = {onClickCountUp} に変更 */}
</div>
);
});
export { Child };
GrandChild.jsx
import { memo } from "react"; //memoをimport
const GrandChild = memo(({onClickCountUp}) => { //GrandChildをメモ化
console.log("GrandChildをレンダリングしました。")
return(
<div style = {{ border : "2px solid #ff8000", width: "80%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#ff8000", padding: "3px 10px", color: "#fff"}}>GrandChild</span></div>
{/* <p>{`GrandChild's props=${stateOfChild}`}</p>を削除 */}
<button onClick = {onClickCountUp} >count up Child's state</button> {/* 新たに追加 */}
</div>
);
});
export {GrandChild};
追加修正箇所にコメントを記述しています。変更点について概要を説明します。Childコンポーネントにおいては、"count up Child's state"ボタンを削除しました。また、GrandChildコンポーネントへ渡すPropsの内容をnumからonClickCountUp関数に変更しました。GrandChildコンポーネントにおいては、Propsの内容を表示するメッセージを削除しました。代わりに元々Childコンポーネントにあった"count up Child's state"のボタンをGrandChildコンポーネントに設置しました。またPropsの内容に変更がない場合の再レンダリング防止のために、コンポーネントをmemo化しました。
memo化されたのでGrandChildコンポーネントはonClickCountUp関数に変更がない限り再レンダリングされないはずですが、"count up Child's state"をクリックすると以下の状態になります。
コンソールに初回レンダリングの表示に加えて"Childをレンダリングしました。"と"GrandChildをレンダリングしました。"の表示が追加されました。
コンソールの表示より、Child、GrandChildコンポーネント共に再レンダリングされてしまっていることが確認できます。ChildコンポーネントはStateの更新により再レンダリングされることはこれまでの説明で理解できますが、Propsの内容は変更されていないように見えるのでGrandChildコンポーネントが再レンダリングされるのは不自然に思えます。これは親コンポーネントにおける関数の再生成が原因です。コンポーネントが再レンダリングされると内部で定義されている関数は内容に変更がなくても再生成されます。その関数をPropsで受け取るコンポーネントは関数の再生成をPropsの内容変更と判定し、再レンダリングを実行します。これを防ぐ方法として、関数のメモ化があります。関数のメモ化の方法は以下になります。
const 関数名 = useCallback(関数の内容, [関数の再生成を実行する変数]);
例えば、"a"と"b"という変数を使用する関数"funcWithAandB"を"a"と"b"の変更時のみ再生成する場合、以下のように記述します。
const funcWithAandB = useCallback(() => func(a, b);), [a,b]);
プログラム実行開始時に関数を生成して使いまわす場合、useCallbackの第二引数に空の配列を設定します。useCallbackを使用して、onClickCountUp関数の定義を書き換えると以下のようになります。
Child.jsx
import { useState, memo, useCallback } from "react";
import { GrandChild } from "./GrandChild"
const Child = memo(() => {
const [num, setNum] = useState(0);
console.log("Childをレンダリングしました。");
const onClickCountUp = useCallback(() => {
setNum(num+1);
},[]);
return(
<div style = {{ border : "2px solid #0080ff", width: "90%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#0080ff", padding: "3px 10px", color: "#fff"}}>Child</span></div>
<p>{`Child's state=${num}`}</p>
{/*<button onClick = {onClickCountUp} >count up Child's state</button> を削除*/ }
<GrandChild onClickCountUp = {onClickCountUp} /> {/* stateOfChild = {num} を onClickCountUp = {onClickCountUp} に変更 */}
</div>
);
});
export { Child };
useCallbackはreactからimportします。ChildコンポーネントのonClickReset関数にuseCallbackを適用後、プログラムを実行し、"count up Child's state"をクリックすると以下の状態になります。
コンソールに"Childをレンダリングしました。"の表示のみ追加されました。
ボタンのクリックにより、コンソールに"Childをレンダリングしました。"の表示のみが追加されていることが確認できます。Propsとして渡しているonClickReset関数が再生成されなくなったことで、GrandChildコンポーネントの再レンダリングを防止することができました。
変数をメモ化する(useMemo)
最後に、変数のメモ化について説明します。GrandChildコンポーネントの表示用のスタイルをChildコンポーネントにおいて定義し、PropsとしてGrandChildコンポーネントに渡すように、ChildコンポーネントとGrandChildコンポーネントを編集します。編集するとそれぞれ以下のようになります。
Child.jsx
import { useState, memo, useCallback } from "react";
import { GrandChild } from "./GrandChild"
const Child = memo(() => {
const [num, setNum] = useState(0);
const stylesForGrandChild = { //GrandChildコンポーネントの表示用のスタイルを定義
styleOfDiv : { border : "2px solid #ff8000", width: "80%", margin: "10px 0px 10px 10px"},
styleOfSpan : {background: "#ff8000", padding: "3px 10px", color: "#fff"}
}
console.log("Childをレンダリングしました。");
const onClickCountUp = useCallback(() => {
setNum(num+1);
},[]);
return(
<div style = {{ border : "2px solid #0080ff", width: "90%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#0080ff", padding: "3px 10px", color: "#fff"}}>Child</span></div>
<p>{`Child's state=${num}`}</p>
<GrandChild onClickCountUp = {onClickCountUp} styles = {stylesForGrandChild} /> {/*styles = {stylesForGrandChild}を追加*/}
</div>
);
);
export { Child };
GrandChild.jsx
import { memo } from "react"; //memoをimport
const GrandChild = memo(({onClickCountUp, styles}) => { //Propsとして、stylesを追加で取得
console.log("GrandChildをレンダリングしました。")
return(
<div style = {styles.styleOfDiv} > {/* styles.styleOfDivをstyle属性に適用 */}
<div><span style={styles.styleOfSpan}>GrandChild</span></div> {/* styles.styleOfSpanをstyle属性に適用 */}
<button onClick = {onClickCountUp} >count up Child's state</button>
</div>
);
});
export {GrandChild};
追加修正箇所にコメントを記述しています。それぞれについて説明します。Childコンポーネントにおいて、GrandChildコンポーネントのスタイルを定義する変数stylesForGrandChildを新たに定義し、GrandChildコンポーネントへ渡すPropsのstylesに設定しています。GrandChildコンポーネントにおいては、Propsとしてstylesを追加で取得し、外枠のdiv要素のstyle属性にstyles.styleOfDivを、コンポーネントのラベルとなるspan要素のstyle属性にstyles.styleOfSpanを適用しています。
GrandChildコンポーネントもonClickReset関数もメモ化されているので、GrandChildコンポーネントが再レンダリングされる要素はなさそうですが、"count up Child's state"をクリックすると以下の状態になります。
コンソールに初回レンダリングの表示に加えて"Childをレンダリングしました。"と"GrandChildをレンダリングしました。"の表示が追加されました。
コンソールの表示より、Child、GrandChildコンポーネント共に再レンダリングされてしまっていることが確認できます。これは親コンポーネントにおける変数の再生成が原因です。先ほど関数の再生成の話をしましたが、変数についても再レンダリング時に再生成が発生します。これを防ぐ方法として、変数のメモ化があります。変数のメモ化の方法は以下になります。
const 変数名 = useMemo(変数の内容を返す関数, [変数の再生成を実行する変数]);
例えば、"height"と"weight"という変数によって定義される変数"BMI"を"height"と"weight"の変更時のみ再生成する場合、以下のように記述します。
const BMI = useMemo(() => {return weight/(height*height);} , [height, weight]);
プログラム実行開始時に変数を生成して使いまわす場合、useMemoの第二引数に空の配列を設定します。useMemoを使用して、変数stylesForGrandChildの定義を書き換えると以下のようになります。
Child.jsx
import { useState, memo, useCallback, useMemo } from "react";
import { GrandChild } from "./GrandChild"
const Child = memo(() => {
const [num, setNum] = useState(0);
const stylesForGrandChild = useMemo( () => {
return (
{
styleOfDiv : { border : "2px solid #ff8000", width: "80%", margin: "10px 0px 10px 10px"},
styleOfSpan : {background: "#ff8000", padding: "3px 10px", color: "#fff"}
}
);
}, []
);
console.log("Childをレンダリングしました。");
const onClickCountUp = useCallback(() => {
setNum(num+1);
},[]);
return(
<div style = {{ border : "2px solid #0080ff", width: "90%", margin: "10px 0px 10px 10px"}} >
<div><span style={{background: "#0080ff", padding: "3px 10px", color: "#fff"}}>Child</span></div>
<p>{`Child's state=${num}`}</p>
<GrandChild onClickCountUp = {onClickCountUp} styles = {stylesForGrandChild} /> {/*styles = {stylesForGrandChild}を追加*/}
</div>
);
});
export { Child };
useMemoはreactからimportします。Childコンポーネントの変数stylesForGrandChildにuseMemoを適用後、プログラムを実行し、"count up Child's state"をクリックすると以下の状態になります。
コンソールに"Childをレンダリングしました。"の表示のみ追加されました。
ボタンのクリックにより、コンソールに"Childをレンダリングしました。"の表示のみが追加されていることが確認できます。Propsとして渡している変数stylesForGrandChildが再生成されなくなったことで、GrandChildコンポーネントの再レンダリングを防止することができました。
以上がReactにおける再レンダリングの制御の方法になります。コンポーネント、関数あるいは変数のメモ化を駆使して、レンダリングの最適化を心がけていきましょう。
