LoginSignup
3
0
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

【React】検索ボックス実装時のパフォーマンス改善案②

Last updated at Posted at 2024-01-18

検索ボックスを実装したら画面が重たくなった…
という方向け
その②です

その①
https://qiita.com/nakakobiz0281/items/6acecb0e29824b463fb4

原因:onChangeのたびに不必要な画面レンダリングがされている

処理が重くなりがちな例

example.js
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>

}

不要なレンダリングがされている参考動画
不要なレンダリング.gif

左側のテキストフィールドに文字を入力するたびに、右側のテキストフィールドやリストの要素一つ一つがレンダリングされてしまっています。
これはuseStateで管理している値が更新されたとき、そのuseStateがあるコンポーネント関数が再度レンダリングされる特性があるためです。
(今回の例でいうとonChangeでsetInputTextするたびにExampleが再レンダリングされる。)

したがって同じファイルに記述されてる右側のリストも毎回レンダリングされてしまっています。

このような状態だとリストの要素が増えたり、他コンポーネントが追加されるたびに重たくなってしまいます。

解決策

メモ化して再レンダリングを防ぐ。
今回はuseMemoを使ってみます。

MemoizationList.js
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で返す

メモ化ができればあとは任意の場所に設置する

example.js
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} />

メモ化した後の実際の動き
リストメモ化.gif

右側のテキストフィールドやリストの要素がレンダリングされていません。
このようにメモ化することにより不必要な再レンダリングを防ぐことができます。

注意点

まずはどこの処理に一番時間がかかっているかを探すなどしてロジックを見直しましょう。
その上で画面レンダリングに多くの時間を使っているのであれば、メモ化を検討しましょう。

公式でも「あらゆる場所に useMemo を追加すべきか?」というTipsに「useMemoを利用した最適化が力を発揮するのは、以下のような、ほんの一部のケースに限られます。…」と書かれています。

useMemoの他にもuseCallbackなどメモ化の方法はいくつかありますが、あくまでこれらは「無駄な再レンダリングを防ぐ」というものであり、アプリ全体の処理速度そのものを向上させるものではないことに注意しましょう。

関連記事

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0