Większość poradników dotyczących wydajności Reacta kończy się tą samą złą radą: owiń wszystko w useMemo i useCallback i gotowe. Jeśli poszedłeś za tą radą, prawdopodobnie spowolniłeś swoją aplikację. Te hooki nie są darmowe. Każdy z nich alokuje pamięć, porównuje zależności i przechowuje skasowane wartości. Używane bez celu, stają się narzutem zamiast optymalizacją.

Sprowadźmy to do tego, co naprawdę ma znaczenie.

Co tak naprawdę robi każdy hook

useMemo zapamiętuje wartość. Przekazujesz mu funkcję, która wykonuje ciężką pracę, a on zwraca wynik. Podczas następnego renderowania, jeśli Twoje zależności się nie zmieniły, React pomija obliczenia i zwraca stary wynik.

useCallback zapamiętuje funkcję. Nie wykonuje on funkcji za Ciebie. Po prostu zwraca tę samą instancję funkcji między renderowaniami, dopóki jej zależności pozostają takie same.

To jest cała różnica. Jeden buforuje obliczoną wartość. Drugi buforuje referencję. Pomylenie ich prowadzi do kodu, który wygląda na zoptymalizowany, ale zachowuje się identycznie jak kod bez nich, generując przy tym dodatkowe zużycie pamięci.

Dlaczego tożsamość funkcji psuje Twoje drzewo

Gdy komponent renderuje się ponownie, React ponownie wykonuje całe ciało funkcji. Każda zmienna jest tworzona na nowo. Każda funkcja inline otrzymuje zupełnie nowy adres w pamięci.

W JavaScript dwie funkcje zawierające dokładnie tę samą logikę nie są równe. () => {} === () => {} zwraca false. Ta sama zasada dotyczy obiektów i tablic. Jeśli komponent nadrzędny definiuje handleSubmit i przekazuje go do dziecka, dziecko otrzymuje nowy prop przy każdym renderowaniu. Nawet jeśli dziecko jest owinięte w React.memo, nie może ono stwierdzić, że nowa funkcja robi to samo co stara. Referencja się zmieniła, więc dziecko renderuje się ponownie.

To jest główny problem, który useCallback miał rozwiązać. Nie chodzi o prędkość. Chodzi o stabilność.

Kiedy useMemo się opłaca

Potrzebujesz useMemo, gdy wykonujesz pracę, która jest obiektywnie kosztowna i możesz zauważyć mierzalne opóźnienie.

Pomyśl o filtrowaniu ogromnego zbioru danych. Jeśli masz tabelę z dziesiątkami tysięcy wierszy i pole wyszukiwania, możesz napisać coś takiego wewnątrz swojego komponentu:

const visibleRows = rows.filter(r => r.name.includes(query));

Bez useMemo ta pętla wykonuje się przy każdym renderowaniu. Jeśli użytkownik kliknie przycisk przełączający pasek boczny, komponent nadrzędny renderuje się ponownie, a Twój filtr uruchamia się jeszcze raz, mimo że rows i query nigdy się nie zmieniły. Przy dużym zbiorze danych to szarpnięcie jest widoczne.

useMemo naprawia to, utrwalając wynik:

const visibleRows = useMemo(() => {
  return rows.filter(r => r.name.includes(query));
}, [rows, query]);

Teraz React uruchamia ten filtr tylko wtedy, gdy zależności faktycznie się zmienią.

Ta sama logika dotyczy złożonych obliczeń matematycznych, przekształcania odpowiedzi z API na formaty przyjazne dla wykresów lub wyprowadzania stanu, który w przeciwnym razie byłby stale przeliczany.

Istnieje drugi, mniej oczywisty przypadek użycia. Jeśli stworzysz obiekt lub tablicę lokalnie i umieścisz je w tablicy zależności useEffect, możesz przypadkowo wywołać ten efekt przy każdym renderowaniu. Obiekty i tablice inline otrzymują za każdym razem nowe tożsamości, więc efekt widzi zmienioną zależność i uruchamia się ponownie. Zmemoizowanie tego obiektu za pomocą useMemo utrzymuje stabilną referencję i pozwala efektowi uruchomić się tylko wtedy, gdy dane źródłowe faktycznie się zmienią.

Kiedy useCallback staje się niezbędny

useCallback ma największe znaczenie, gdy przekazujesz handlery do komponentów dzieci, które są zoptymalizowane za pomocą React.memo.

Wyobraź sobie komponent nadrzędny, który przechowuje licznik. Renderuje on również kosztowną listę dzieci:

function Parent() {
  const [count, setCount] = useState(0);
  
  const handleItemClick = (id) => {
    console.log(id);
  };
  
  return (
    <div>
      <button onClick={() => setCount(c + 1)}>{count}</button>
      <ExpensiveList onItemClick={handleItemClick} />
    </div>
  );
}

Za każdym razem, gdy count się zmienia, Parent renderuje się ponownie. Tworzona jest nowa funkcja handleItemClick. Ponieważ ExpensiveList otrzymuje nową referencję propa, również się renderuje. Jeśli ExpensiveList jest owinięty w React.memo, ta memoizacja jest całkowicie zmarnowana, ponieważ prop funkcji uległ zmianie.

useCallback zachowuje referencję:

const handleItemClick = useCallback((id) => {
  console.log(id);
}, []);

Teraz ExpensiveList renderuje się ponownie tylko wtedy, gdy naprawdę tego potrzebuje.

Inna krytyczna sytuacja dotyczy useEffect. Jeśli efekt subskrybuje funkcję zdefiniowaną wewnątrz Twojego komponentu, a funkcja ta zmienia tożsamość przy każdym renderowaniu, efekt będzie wielokrotnie usuwany i subskrybowany ponownie. Zmemoizowanie funkcji utrzymuje efekt w stabilności.

Pułapka tablicy zależności i przestarzałe domknięcia

Oba hooki polegają na tablicach zależności i to właśnie tutaj kryje się większość błędów.

If you omit a variable from the dependency array, your memoized function or value closes over an old version of that variable. This is a stale closure. The UI might display fresh data, but your callback is still looking at state from three renders ago. The fix is simple but easy to miss during code reviews: include every value used inside the hook that could change.

Run the react-hooks/exhaustive-deps ESLint rule. It will catch obvious omissions. But do not treat it as a robot. Understand why each dependency matters.

The Hidden Tax of Over-Optimization

Beginners often armor every function and every value with these hooks because it feels safe. That habit backfires.

React must store cached values in memory. On every render, it must iterate through your dependency array and compare each item using Object.is. That comparison is cheap, but it is not free. If you wrap a trivial event handler like onClick={() => setOpen(true)} inside useCallback, you are paying memory and CPU costs to avoid creating a function that would have been instant to allocate.

The hooks also add noise. Code wrapped in useMemo and useCallback is harder to read and harder to maintain. Every dependency array is a potential stale closure waiting to bite you.

The real rule of thumb is unglamorous but effective: write plain code first. Optimize only when you have proof of a problem. Use the React DevTools Profiler to identify which components are expensive and which renders are wasteful. If a render takes less than a few milliseconds, no user will notice, and your memoization is solving nothing.

The Bottom Line

useMemo is for expensive values. useCallback is for stable function references. Neither hook makes your component render faster on its own; they prevent unnecessary downstream work. Start without them, measure with actual tooling, and add them precisely where the profiler shows a bottleneck. Clean code that rerenders occasionally will almost always beat over-engineered code that memoizes everything.