検索ボックスを実装したら画面が重たくなった…
という方向け
その②です
その①
https://qiita.com/nakakobiz0281/items/6acecb0e29824b463fb4
原因:onChangeのたびに不必要な画面レンダリングがされている
処理が重くなりがちな例
export function Example() {
const [nameList, setNameList] = useState(nameListData)
// ↓画面にレンダリングするのに高負荷がかかる長さの配列
const [veryLongList, setVeryLongList] = ([])
const filteringName = (inputValue) => {
if (inputValue) {
const filteredNames = nameListData.filter((name) =>
name.toLowerCase().includes(inputValue.toLowerCase())
);
setNameList(filteredNames);
} else {
setNameList(nameListData);
}
};
...
// 左側のリスト
<Box sx={{ height: "50vh", width: "300px", padding: "15px" }}>
<TextField
margin="normal"
fullWidth
onChange={(e) => setInputText(e.currentTarget.value)}
/>
<Box sx={{ height: "80%", overflow: "auto" }}>
<List>
{nameList.length > 0 ? (
nameList.map((name) => (
<ListItem key={name}>
<ListItemAvatar>
<Avatar>{name.substr(0, 1)}</Avatar>
</ListItemAvatar>
<ListItemText>{name}</ListItemText>
</ListItem>
))
) : (
<ListItem>
<ListItemText>検索結果が見つかりませんでした</ListItemText>
</ListItem>
)}
</List>
</Box>
</Box>
// 右側のリスト
<Box sx={{ height: "50vh", width: "300px", padding: "15px" }}>
<TextField
margin="normal"
fullWidth
onChange={(e) => setAnotherFunction(e.currentTarget.value)}
placeholder="左の処理と全く関係ないリスト"
/>
<Box sx={{ height: "80%", overflow: "auto" }}>
<List>
// ↓表示させる内容は変わらないのに,
// veryLongListが毎回レンダリングされる
{veryLongList.length > 0 ? (
veryLongList.map((name) => (
<ListItem key={name}>
<ListItemAvatar>
<Avatar>{name.substr(0, 1)}</Avatar>
</ListItemAvatar>
<ListItemText>{name}</ListItemText>
</ListItem>
))
) : (
<ListItem>
<ListItemText>検索結果が見つかりませんでした</ListItemText>
</ListItem>
)}
</List>
</Box>
</Box>
}
左側のテキストフィールドに文字を入力するたびに、右側のテキストフィールドやリストの要素一つ一つがレンダリングされてしまっています。
これはuseStateで管理している値が更新されたとき、そのuseStateがあるコンポーネント関数が再度レンダリングされる特性があるためです。
(今回の例でいうとonChangeでsetInputTextするたびにExampleが再レンダリングされる。)
したがって同じファイルに記述されてる右側のリストも毎回レンダリングされてしまっています。
このような状態だとリストの要素が増えたり、他コンポーネントが追加されるたびに重たくなってしまいます。
解決策
メモ化して再レンダリングを防ぐ。
今回はuseMemoを使ってみます。
export const MemoizationList = (data) => {
return useMemo(() => {
return (
<Box sx={{ height: "50vh", width: "300px", padding: "15px" }}>
<TextField
margin="normal"
fullWidth
placeholder="左の処理と全く関係ないリスト"
/>
<Box sx={{ height: "80%", overflow: "auto" }}>
<List>
{data.data.length > 0 ? (
data.data.map((name) => (
<ListItem key={name}>
<ListItemAvatar>
<Avatar>{name.substr(0, 1)}</Avatar>
</ListItemAvatar>
<ListItemText>{name}</ListItemText>
</ListItem>
))
) : (
<ListItem>
<ListItemText>検索結果が見つかりませんでした</ListItemText>
</ListItem>
)}
</List>
</Box>
</Box>
);
}, [data]);
};
手順
①メモ化したいリストをコンポーネント化する
②コンポーネントをuseMemoでラップする
③veryLongListの値が変わらない限り再レンダリングしないように、第二引数にveryLongListを設定する
④メモ化したものをreturnで返す
メモ化ができればあとは任意の場所に設置する
const [nameList, setNameList] = useState(nameListData)
// ↓画面にレンダリングするのに高負荷がかかる長さの配列
const [veryLongList, setVeryLongList] = ([])
const filteringName = (inputValue) => {
if (inputValue) {
const filteredNames = nameListData.filter((name) =>
name.toLowerCase().includes(inputValue.toLowerCase())
);
setNameList(filteredNames);
} else {
setNameList(nameListData);
}
};
...
// 左側のリスト
<Box sx={{ height: "50vh", width: "300px", padding: "15px" }}>
<TextField
margin="normal"
fullWidth
onChange={(e) => setInputText(e.currentTarget.value)}
/>
<Box sx={{ height: "80%", overflow: "auto" }}>
<List>
{nameList.length > 0 ? (
nameList.map((name) => (
<ListItem key={name}>
<ListItemAvatar>
<Avatar>{name.substr(0, 1)}</Avatar>
</ListItemAvatar>
<ListItemText>{name}</ListItemText>
</ListItem>
))
) : (
<ListItem>
<ListItemText>検索結果が見つかりませんでした</ListItemText>
</ListItem>
)}
</List>
</Box>
</Box>
// メモ化した右側のリスト
<MemoizationList data={veryLongList} />
右側のテキストフィールドやリストの要素がレンダリングされていません。
このようにメモ化することにより不必要な再レンダリングを防ぐことができます。
注意点
まずはどこの処理に一番時間がかかっているかを探すなどしてロジックを見直しましょう。
その上で画面レンダリングに多くの時間を使っているのであれば、メモ化を検討しましょう。
公式でも「あらゆる場所に useMemo を追加すべきか?」というTipsに「useMemoを利用した最適化が力を発揮するのは、以下のような、ほんの一部のケースに限られます。…」と書かれています。
useMemoの他にもuseCallbackなどメモ化の方法はいくつかありますが、あくまでこれらは「無駄な再レンダリングを防ぐ」というものであり、アプリ全体の処理速度そのものを向上させるものではないことに注意しましょう。
関連記事