JSとReactにおけるメモ化


社内勉強会のメモ。


What is メモ化

メモ化(英: Memoization)とは、プログラムの高速化のための最適化技法の一種であり、サブルーチン呼び出しの結果を後で再利用するために保持し、そのサブルーチン(関数)の呼び出し毎の再計算を防ぐ手法である。

キャッシュはより広範な用語であり、メモ化はキャッシュの限定的な形態を指す用語である。

by wikipedia

つまり、 キャッシュ ⊃ メモ化


わかりやすいので、Rubyで頻繁に使うメモ化の例

hoge.rb
def hoge
  @hoge ||= something_heavy_subroutine
end
fuga.rb
def fuga
  @fuga ||=
    begin
      result1 = something_heavy_subroutine1
      result2 = something_heavy_subroutine2(result1)
      something_heavy_subroutine3(result2)
    end
end

余談
ちなみにメモって言葉、Rubyのinject(reduce)にも使われています。

inject.rb
enum.inject {|memo, item| block }
enum.inject(init) {|memo, item| block }


jsのreduceと同じですが、jsのreduceaccumulatorという単語を使ってる。

reducer.js
const reducer = (accumulator, currentValue) => accumulator + currentValue;


じゃあjsでメモ化のコード書くとどうなるの

memo_ex.js
// a simple memoized function to add something
const memoizedAdd = () => {
  let cache = {};
  return (n) => { // クロージャなので一回リターンされたこの関数はcacheのデータを持ち続ける
    if (n in cache) { // キャッシュにあるかどうか見てる
      console.log('Fetching from cache');
      return cache[n]; // キャッシュに存在すればそれを返す
    }
    else { // 存在しない場合は普通に計算して、キャッシュに保存しておく
      console.log('Calculating result');
      let result = n + 10;
      cache[n] = result;
      return result;
    }
  }
}
// returned function from memoizedAdd
const newAdd = memoizedAdd();
console.log(newAdd(9)); // calculated
console.log(newAdd(9)); // cached


ただし、前提として、Rubyはクラスを使うのでインスタンス変数に保存するけど、

  • jsではクラスをなるべく使わない雰囲気がある(特にReactとか使ってるとそう)ので、関数単位でメモ化ができて欲しい
  • クロージャ使えるから関数単位でも簡単にメモ化を実現できる

という背景がある。


つまり、クラスを使うならjsのメモ化だってこれで良い。

hoge_class.js
class Hoge {
  constructor() {
    this._heavy_calculate_result;
  }

  heavy_calculate() {
    if (this._heavy_calculate_result != undefined) return this._heavy_calculate_result;

    const result = // something calculate

    this._heavy_calculate_result = result;

    return result;
  }
}

要は、メモ化自体は概念であって、特定の実装に依存するものではない。
クロージャを使うと関数単体でメモ化できるから、jsではそれが適してる時が多いかもね、っていう話。
計算がなんども走る場合は気にしてメモ化しておこう。


React Hooksにおけるメモ化

React Hooksが用意しているメモ化のためのフックが二つある。

  • React.useMemo
  • React.useCallback

Reactのコンポーネントの場合、renderは割と高頻度で呼ばれるので、renderの中で重い処理を繰り返すとパフォーマンスが落ちるので、適切にメモ化していきたい。


useMemo

usememo.js
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useCallback

callback.js
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useCallback(fn, deps) は useMemo(() => fn, deps) と等価です。


このuseCallbackによる関数のメモ化は、計算処理を緩和するという意味はほとんどない。
関数の生成コストと前の値との比較コストが、なんなら前者の方が少ない可能性もある。

ただし、子のコンポーネントがPureComponent、もしくはReact.memoを使用したコンポーネントだった場合、本来なら再レンダリングが走らないべきところでレンダリングが走ってしまう可能性がある。


re-rendering.js
const Component = (props: Props) => {
  const onChange = () => {
    props.value; // なんかpropsを使った処理
  };

  return <HogePureComponent onChange={onChange}>
};

useCallbackを使用しない場合、renderが走るたびに関数が生成されるわけだが、そうなるとPureComponent(もしくはReact.memoなコンポーネント)に渡る関数の参照が毎回変わるため、意図としては再レンダリングしないはずの場所で再レンダリングが走ってしまう。

(子のコンポーネントがPureでない場合は関係なく毎回走る。)


re-rendering.js
const Component = (props: Props) => {
  const memoizedOnChange = React.useCallback(() => {
    props.value; // なんかpropsを使った処理
  }, [props.value]);

  return <HogePureComponent onChange={memoizedOnChange}>
};

こうすると、props.valueが変わらない限り参照が毎回同じになるため、不要なレンダリングが防がれる。


ただしこの辺り、deps(Dependency List)(第二引数の配列、他のフックと同じ)をちゃんと書かないと値を更新したのに表示が更新されないみたいな事が起こり得るので、実装時は要注意。