LoginSignup
26
16

More than 1 year has passed since last update.

JSとReactにおけるメモ化

Last updated at Posted at 2019-08-08
1 / 17

社内勉強会のメモ。


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]);

これがコンポーネント内でのメモ化の基本。deps が変わらない限り、渡した関数の返り値の同じ参照を返す。


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でない場合は関係なく毎回走る。
ここよく間違いやすいので注意。ただ useCallbackにして子コンポーネントに渡す参照を同じにしただけでは、レンダリングが防がれるわけではない。「不要なレンダリングが防がれる」のはあくまでも PureComponent や React.memo (React.memo と React.useMemo は異なるのでそこも注意) を併用していることが前提。


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

26
16
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
26
16