Posty

Zrozum memoizację w kilka minut na przykładzie React’owego useMemo()

Zrozum memoizację w kilka minut - zdjęcie

Na czym polega memoizacja?

Memoizacja to technika optymalizacyjna, stosowana głównie, aby poprawić szybkość działania programu komputerowego poprzez zapamiętywanie rezultatu wywołań funkcji i zwracanie zapamiętanej (w żargonie IT: zkeszowanej od eng. cache) wartości, gdy argumenty funkcji się nie zmieniły.

Kiedy używać memoizacji? - przykłady

Techniki memoizacji warto użyć, gdy funkcja, której rezultat chcemy zapamiętać (w żargonie IT: zkeszować, zmemoizować):

  • jest tzw. “pure function”, czyli funkcją, która nie zawiera side effect’ów, więc dla danego X zawsze zwraca taki sam rezultat, TUTAJ jest super wyjaśnienie czym są pure functions,
  • zawiera kosztowne obliczenia, które potrzebują sporo czasu i zasobów, aby zwrócić ów rezultat,
  • wykonuje zapytania do API. Jeśli argument wywołania tej funkcji jest taki sam jak przy poprzednim wywołaniu, to rezultat zostanie zwrócony z pamięci (cache),
  • w przypadku funkcji, które wywołują same siebie z powtarzającymi się argumentami, np. funkcję rekursywne.

Memoizacja - prosty przykład implementacji

Zerknij na poniższy przykład kodu i zwróć uwagę na komentarze, które wyjaśniają co się dokładnie dzieje. Jest to prosta funkcja, która podaną liczbę dzieli zawsze razy 9. Kluczowe jest tutaj słowo zawsze - sugeruje ono, że jest to “pure function”.

const cache = {} // stwórz obiekt pamięci - cache, 
                 // który posłuży do przechowania zapamiętywanego rezultatu

function multiplyByNine(input) {
  // sprawdź czy podana liczba "input" nie była już przypadkiem zapamiętana
  if (!cache.hasOwnProperty(input)) {
    // zapamiętaj rezultat mnożenia przez 9 dla podanej liczby "input"
    cache[input] = input * 9  
  }

  return cache[input] // zwróć zapamiętany rezultat
}

A tak to zadziała w praktyce (wyjaśnienie w komentarzach w kodzie):

multiplyByNine(1); // funkcja zwróci 9 jako wynik działania 1 * 9
multiplyByNine(1); // funckja zwróci 9, natomiast wynik zostanie zwrócony 
                   // z pamięci bez ponownego wykonywania obliczeń ponieważ argument podany do funkcji był nadal taki sam

Proste i zrozumiałe, prawda?

P. S. Przykład kodu zaczerpnąłem od Kent C. Dodds’a ze strony: https://epicreact.dev/memoization-and-react/

React useMemo() - kod źródłowy, czyli jak to wygląda pod maską?

Przykładem implementacji techniki memoizacji jest React’owy hook o nazwie useMemo(). Nawet jeśli nie znasz React’a, przeanalizuj implementację tej funkcji wraz z komentarzami, a zauważysz pewne analogie do poprzedniego przykładu 🙂

function updateMemo<T>(
  nextCreate: () => T, // podaj funkcję której rezultat chcesz zmemoizować
  deps: Array<mixed> | void | null, // podaj tablicę zależności, których zmiany chcesz obserwować
): T {
  // stwórz obiekt hook, który jest odpowiednikiem obiektu cache z poprzedniego przykładu
  const hook = updateWorkInProgressHook();
  // sprawdź czy podana tablica zależności istnieje, jeśli tak to zwróć te tablice, jeśli nie zwróć null
  const nextDeps = deps === undefined ? null : deps;
  // odczytaj poprzednio zapamiętany stan (poprzedni cache)
  const prevState = hook.memoizedState;

  // jeśli poprzedni zapamiętany rezultat istnieje w pamięci
  if (prevState !== null) {
    // jeśli tablica zależności istnieje
    if (nextDeps !== null) {
      // odczytaj poprzednią tablice zależności
      const prevDeps: Array<mixed> | null = prevState[1];
      // jeśli poprzednia i obecna tablica zależności jest taka sama
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // zwróć zapamiętany poprzednio rezultat z pamięci
        return prevState[0]; 
      }
    }
  }
 
  // w przeciwnym przypadku
  // wykonaj podaną pierwotnie funkcję i przypisz jej rezultat do nextValue
  const nextValue = nextCreate();
  
  // zapamiętaj nowy rezultat(nextValue) oraz nową tablicę zależności(nextDeps)
  hook.memoizedState = [nextValue, nextDeps];

  // zwróć nowy rezultat
  return nextValue;  
}

Jeśli przeczytałeś dokładnie komentarze, linijka po linijce to na pewno zauważyłeś analogię do przykładu opisanego wcześniej. Jeśli nie rozumiesz każdej linijki kodu pochodzącej z implementacji hooka useMemo — nie istotne. Chodziło mi tutaj tylko o to, abyś zauważył/a, zastosowanie techniki memoizacji, którą często używają na co dzień developerzy React.

PS. Dokładny kod źródłowy useMemo() znajdziesz TUTAJ

To koniec! Powinieneś teraz już rozumieć jak działa memoizacja 😀

🚨🚨🚨 UWAGA BONUS dla tych, którzy lubią wiedzieć więcej

Kiedy nie używać memoizacji?

  1. Nie staraj się stosować techniki memoizacji wszędzie i za wszelką cenę. Optymalizacja wydajności to koszt związany z poświęceniem Twojego czasu — jeśli aplikacja działa płynnie i nie zauważasz żadnych realnych spowolnień działania programu, to nie optymalizuj.
  2. Wywołanie funkcji memoizującej samo w sobie jest kosztem pod względem czasu i zasobów. To kolejny argument, poświadczający, że nie warto memoizować wszystkiego, co się da, tylko dla zasady czy własnego widzimisię.
  3. Nie memoizuj funkcji, które nie są “pure” i zawierają side effect’y, może to doprowadzić do niepoprawnego i nieprzewidywalnego działania programu.

Pozdrawiam i do następnego 👋