【React】メモ化を行う基準

media thumbnail

こんにちは、株式会社Gizumoでエンジニアをしている田中です。

React開発を行う上で、メモ化を行う基準が分からないという方が多いのではないでしょうか。私も何度も調べては答えが出ず、結局は「とりあえずメモ化しておけば良いんじゃない?」という感じでした。
しかし、2023年3月に正式リリースされた新たなReactの公式ドキュメントでメモ化に関する詳細がかなりアップデートされておりとても参考になるため、今回はその内容を元にメモ化を行う基準についてまとめていきます。

TL;DR

  • メモ化を効率的に行いたい場合、「必要なタイミングでのみメモ化を行い、それ以外ではメモ化しない
  • メモ化をしたいが効率的に行うためのリソースを割けない場合、「個々のケースについて考えず、できるだけメモ化することを選択する

前提・対象とする読者

  • サンプルコードはReactで記述

また、対象とする読者は以下のような方を想定しています。

  • Reactで開発を行っている方
  • メモ化を行う基準について知りたい方

メモ化とは

メモ化とは、変数・関数・コンポーネントのキャッシュを再利用しコンポーネントの不要な再レンダリングを抑えることです。目的はアプリケーションパフォーマンスの最適化です。

メモ化には、memouseMemouseCallbackというReactAPIhooksが使われます。それぞれの使用に必要なタイミングについて見ていきましょう。

参考

memo

memoは、コンポーネントをメモ化するための高階コンポーネント(Higher-Order Component)です。memoを使用すると、コンポーネントの再レンダリングを最適化できます。コンポーネントの再レンダリングは、コンポーネントへのpropsが変更された場合に発生します。そのため、memoは以下のような場合に使用することが推奨されています。

memoが必要なタイミング

memoが必要なタイミングは、以下のような場合です。

  • コンポーネントがpropsの変更によって再レンダリングされる可能性が低い場合
  • コンポーネント内での処理が重く、その処理が同じ結果を返す場合

コンポーネントがpropsの変更によって再レンダリングされる可能性が低い場合

通常、親コンポーネントから渡されるpropsが変更されるか親コンポーネントが再レンダリングされると、子コンポーネントは再レンダリングされます。しかし、propsが頻繁に変更されない場合、そのコンポーネントをmemoでラップすることで、親コンポーネントの再レンダリングによる子コンポーネントの再レンダリングを無くすことができます。それによって子コンポーネントの再レンダリング条件をpropsの変更のみに絞ることができます。よってコンポーネントがpropsの変更によって再レンダリングされる可能性が低い場合、再レンダリングの回数をかなり減らすことができます。

コンポーネント内での処理が重く、その処理が同じ結果を返す場合

もしコンポーネント内で重い計算や処理が行われており、その結果が常に同じである場合、memoを使用することでキャッシュを利用できます。memoは、コンポーネントの入力が変更されない限り、以前の結果を再利用します。これにより、再計算を避けることができます。とくに、計算量が多い場合や、外部のリソースへのアクセスが必要な場合には、memoを使用して結果をキャッシュすることで効率化が図れます。

import { memo } from 'react';

export const HeavyCalculationComponent = memo((props) => {
  // 重い計算や処理を行う関数
  const performHeavyCalculation = (value) => {
    // 重い計算や処理の実装
    // 例えば、非常に時間のかかるループ処理や再帰処理などを想定
    let result = 0;
    for (let i = 0; i < value; i++) {
      result += i;
    }
    return result;
  };

  // propsが変更されない限り、同じ結果を返す(useMemoについては後述)
  const sameResultEveryTime = performHeavyCalculation(props.value)

  return (
    <div>
      <p>入力値: {props.value}</p>
      <p>計算結果: {sameResultEveryTime}</p>
    </div>
  );
});

参考:Should you add memo everywhere?

useMemo

useMemoは、指定した関数の結果をメモ化するためのフックです。関数の実行結果をキャッシュし、依存する値が変化しない限り、キャッシュされた値を再利用します。これにより、重い計算や処理を最適化できます。そのため、useMemoは以下のような場合に使用することが推奨されています。

useMemoが必要なタイミング

useMemoが必要なタイミングは、以下のような場合です。

  • 関数内での計算や処理が重く、依存関係がほとんど変化しない場合
  • 計算結果を再利用する必要がある場合

関数内での計算や処理が重く、依存関係がほとんど変化しない場合

このような場合、同じ入力に対して同じ結果を返す関数を再評価する必要がありません。useMemoを使うことで、関数の再評価を最適化し、不要な計算を避けることができます。

以下は、このようなケースでのuseMemoの使用例です。

import { useMemo } from 'react';

export const HeavyCalculationComponent = (props) => {
  const performHeavyCalculation = (value) => {
    // 重い計算や処理の実装
    // 例えば、非常に時間のかかるループ処理や再帰処理などを想定
    let result = 0;
    for (let i = 0; i < value; i++) {
      result += i;
    }
    return result;
  };

  // propsが変更されない限り、同じ結果を返す
  const memoizedResult = useMemo(() => performHeavyCalculation(props.value), [props.value]);

  return (
    // JSXを返す
  );
};

計算結果を他のコンポーネントや処理で再利用する場合

これにより、同じ結果を持つコンポーネントや処理間で計算を再実行する必要がなくなり、パフォーマンスが向上します。

import { useMemo } from 'react';

type Props = {
  data: number[];
}

export const CalculationComponent = ({ data }: Props) => {
  const memoizedResult = useMemo(() => {
    // 計算や処理を行う
    // 例えば、dataの要素の合計を計算する処理などを想定
    let sum = 0;
    for (let i = 0; i < data.length; i++) {
      sum += data[i];
    }
    return sum;
  }, [data]);

  useEffect(() => {
    // memoizedResultを使用した処理
  }, [memoizedResult])

  return (
    // JSXを返す
  );
};

参考:Should you add useMemo everywhere?

useCallback

useCallbackは、指定した関数をメモ化するためのフックです。コールバック関数をキャッシュし、依存する値が変化しない限り、同じコールバック関数のインスタンスを再利用します。そのため、useCallbackは以下のような場合に使用することが推奨されています。

useCallbackが必要なタイミング

useCallbackが必要なタイミングは、以下のような場合です。

  • 関数をメモ化されたコンポーネントにpropsとして渡す場合
  • 関数が何らかのhooksの依存関係として使用される場合

関数をメモ化されたコンポーネントにpropsとして渡す場合

子コンポーネントにpropsとして関数を渡す必要があるとき、その関数がメモ化されていれば、その関数の再生成回数は少なくなるため子コンポーネントの再レンダリングを抑えることができます。(そのメモ化された関数が持つ依存関係の変更回数に限定できる。)

  • Propsとして渡す関数をメモ化していない時、親コンポーネントのレンダリング時に毎回レンダリングされている(6回のカウントアップに合わせてCounterComponentが6回レンダリングされている)
  • Propsとして渡す関数をメモ化している時、親コンポーネントのレンダリング時にレンダリングされない(関数の依存関係変更がないため関数の再生成が起きず、それに合わせてCounterComponentのレンダリングが抑えられている)
  • ソースコード
// 子コンポーネント

import { memo } from "react";

type Props = {
  counter: () => void;
}

export const CounterComponent = memo(({ counter }: Props) => {
  console.log('render CounterComponent');
  return (
    <div>
      <h2 style={{ color: 'blue'}}>Counter Component</h2>
      <button onClick={counter}>count up in CounterComponent</button>
    </div>
  )
})
// 親コンポーネント

import { useCallback, useState } from 'react'
import './App.css'
import { CounterComponent } from './CounterComponent';

function App() {
  const [count, setCount] = useState(0)
  const [countV2, setCountV2] = useState(0)

  // Propsとして渡す関数
  const counter = useCallback(() => {
    setCount((prev) => prev + 1);
  }, [setCount]);

  const counterV2 = useCallback(() => {
    setCountV2((prev) => prev + 1);
  }, [setCountV2]);

  return (
    <>
      <p>{count}</p>
      <button onClick={counter}>count up</button>
      <p>{countV2}</p>
      <button onClick={counterV2}>count up V2</button>
      <br />
      <CounterComponent counter={counter} />
    </>
  )
}

export default App

関数が何らかのhooksの依存関係として使用される場合

useCallbackは、他のHooksの依存関係として使用される場合にも有用です。たとえば、useEffectuseMemoなどのHooksでは、特定の関数が変更された場合に処理を実行したり、値を再計算したりすることがあります。その際、依存関係の関数が頻繁に変更されない場合、useCallbackを使用して関数をキャッシュし、不要な処理や再計算を防ぐことができます。

import { useState, useEffect, useCallback } from 'react';

export const CounterComponent = () => {
  const [count, setCount] = useState(0);

  // カウントアップのコールバック関数をメモ化
  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, [setCount]);

  // increment関数に依存して副作用を実行
  useEffect(() => {
    console.log('Increment function changed');
    // 他の処理や副作用を実行
    // ...
  }, [increment]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

参考:Should you add useCallback everywhere?

すべてのhooksに共通していること

すべてのhooksで共通して以下のようなことが書かれています。

There is no benefit to wrapping a function in {hook} in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.

上記を訳すると以下になります。

  • それぞれのhooksの必要なタイミング以外ではメモ化をするメリットはない
  • しかしメモ化することに大きな害もない
  • チームによっては個々のケースについて考えず、できるだけメモ化することを選択する
  • メモ化するデメリットは、コードが読みづらくなること
  • また、すべてのメモ化が有効なわけではない。”常に新しい “1つの値だけで、コンポーネント全体のメモ化が壊れてしまう

参考
Should you add memo everywhere?
Should you add useMemo everywhere?
Should you add useCallback everywhere?

まとめ

これまでのことからメモ化の基準を結論づけると、「必要なタイミングでのみメモ化を行い、それ以外ではメモ化しない」となります。

その基準って難しくない?

しかしこの基準を設けて実際にプロジェクトに導入するのって難しくないでしょうか?実装者全員がそれぞれのhooksに必要なタイミングを判断し適切にメモ化を行いながら実装をするというのは無理があるように思えます。

そういう背景からメモ化の基準は公式に倣い「個々のケースについて考えず、できるだけメモ化することを選択する」とするのは1つの手ではないかと思われます。

参考

少しでも開発にお困りの方は
相談しやすいスペシャリストにお問い合わせください

お問い合わせ
  1. breadcrumb-logo
  2. メディア
  3. 【React】メモ化を行う基準