memo化について
Reactでは親コンポーネントが再レンダリングされると、子のコンポーネントも再レンダリングされてしまいます。
例えば、以下のような例だと親コンポーネントでcount upボタンを押すたびに、
子のExpensiveComponent
も再レンダリングされてしまいます。
ExpensiveComponent
が非常に重い処理が含まれていると、count upボタンを押すたびに重い処理が走ってしまい、パフォーマンス悪化につながってしまいます。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponent/>
</div>
);
}
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
この挙動を防ぐためにmemo化を行います。
以下のように子コンポーネントをmemo化することで、親コンポーネントが再レンダリングされたときでも、子コンポーネントが再レンダリングされることはなくなります。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo/>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
メモ化の問題点
memo化を用いた場合、将来的に開発を行なっていく中で、実装上のちょっとした変更で想定通り動かなくなってしまうという懸念点が挙げられます。
propsを渡すパターン
例えば、次のようにExpensiveComponentにstyleをpropsで渡すようにしたらどうでしょうか?
一見ExpensiveComponentに渡されるものは常に同一なので、想定通り動きそうに思えますが、親コンポーネントのレンダリングに合わせて子コンポーネントもレンダリングされてしまい、memo化が機能しないコードになってしまいました。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo style={{"color":"red"}}/>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent({style}) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p style={style}>expensive</p>
}
これはReactのPropsの差分はObject.is
で同一であれば差分なしでレンダリングしない、同一でなければ差分ありと判断されレンダリングされるためです。(https://ja.react.dev/reference/react/memo#minimizing-props-changes)
Object.is
では両方の値がメモリー内の同じオブジェクトを参照している場合のみtrue
になるので
下記のような場合false
になってしまいます。(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
// falseになってしまう!!
Object.is({"color":"red"},{"color":"red"})
この挙動によってobject
の中身は実際には変わっていないにもかかわらず、Reactの内部的には変わってしまったものと判断され再レンダリングが走ってしまいます。
これを解決するためにはuseMemo
を使います。
これによりmemoStyle
はObject.is
での比較でも同一のものと判断され
子コンポーネントが再レンダリングされることはなくなります。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
const memoStyle = React.useMemo(()=>({"color":"red"}),[])
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo style={memoStyle}/>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent({style}) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p style={style}>expensive</p>
}
しかし、渡すpropsが増えた際に全ての開発者がこの挙動を予期してちゃんとuseMemo
を使用できるかというと怪しい部分があり、いざパフォーマンスの低下が検知されて初めて実装ミスに気づくといったこともあると思います。
このような罠に気づかずハマるおそれがあるというのが、memo化をなるべく避ける理由の一つです。
childrenを渡しているパターン
children
を渡しているパターンも同様に考えてみます。
この場合も、一見ExpensiveComponent
のchildren
は同一なので想定通り動きそうに思えますが、親コンポーネントのレンダリングに合わせて子コンポーネントもレンダリングされてしまい、memo化が機能しないコードになってしまいました。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo>
<p>children</p>
</ExpensiveComponentMemo>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent({children}) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 1000;i++){
i= i+1;
}
return <div>expensive{children}</div>
}
jsx記法はReact.createElement
の糖衣構文であり、実際は新しいobjectが作成されてしまっているので、先ほどの場合と同様にこの事象が起こってしまっています。
こちらの場合もuseMemo
を用いることでレンダリングを防ぐことは可能ですが、
可読性や、変更時の柔軟性を考えるとバグを生みやすいコードになってしまいます。
memoに変わる代替案
ここまで、記述した通りmemo化は思わぬところに落とし穴が多く、複数人で開発し引き継ぎを行っていくようなプロジェクトだと気づかずにバグを混入させてしまう可能性が高く、あまり使用しないことを推奨したいです。
しかし、パフォーマンス上の懸念からどうしても再レンダリングを避けたい時の解決策を紹介します。
stateを用いているコンポーネントを分離させる
count
のstate
を使っているのは一部だけなのでその部分を別のコンポーネントに切り出すというのが一つです。
これによって、count
によって変化を受けるコンポーネントは
CountComponent
だけになるのでcount
の変化によってExpensiveComponent
が再レンダリングを避けることができます。
import React,{useState} from 'react';
export function App() {
return (
<div className='App'>
<h1>Hello React.</h1>
<CountComponent/>
<ExpensiveComponent/>
</div>
);
}
function CountComponent(){
[count,setCount]= useState(0);
return <p>{count}</p>
}
function ExpensiveComponent(props) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return (
<>
<p>expensive</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
</>
)
}
childrenを用いてコンポーネントを分離させる
次のようなDOM構造を維持したい場合、単純にstate
を用いているコンポーネントを分離させることが難しいように思えます。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<div className='count'>{count}</div>
<div>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponent/>
</div>
</div>
);
}
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
この場合でも、下記のようにchildren
として重い処理をしている子コンポーネントを渡しCountComponent
で表示することにより、count
の変化によるExpensiveComponent
の再レンダリングを避けることができます。
import React,{useState} from 'react';
export function App() {
return (
<div className='App'>
<h1>Hello React.</h1>
<CountComponent>
<ExpensiveComponent/>
</CountComponent>
</div>
);
}
dunction CountComponent({children}){
[count,setCount]= useState(0);
return (
<>
<div className='count'>{count}</div>
<div>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
{children}
</div>
</>
);
}
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
まとめ
コンポーネントのメモ化は、実装した際にはパフォーマンスを向上できるものの、その後の保守運用フェーズで気付かぬうちにバグを生み出してしまう可能性も高いです。
今回の代替案でご紹介したものなどmemo化しないで実装できないかをまず考えることが大事だと思いました。
参考文献