Menu

React.memo: pomijanie renderowania przy tych samych propsach

memo owija komponent tak, że React pomija jego ponowne renderowanie, gdy propsy są płytko równe tym z ostatniego razu. Dowiedz się, kiedy pomaga, dlaczego nowe obiekty i funkcje w propsach go unieważniają i czego nie zatrzymuje.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

memo pozwala komponentowi pominąć ponowne renderowanie, gdy jego propsy są takie same jak ostatnio. Owiń komponent w memo(...), a gdy jego rodzic się renderuje, React najpierw porówna każdy nowy prop ze starym; jeśli wszystkie są równe, użyje poprzedniego wyniku, zamiast znowu wywoływać komponent.

Pisz w polu: nadąża, a konsola pokazuje render SlowList tylko raz, z pierwszego renderowania. Usuń opakowanie memo( (i jego nawias zamykający) i pisz ponownie: każde naciśnięcie klawisza czeka teraz 150 ms na listę i wypisuje renderowanie.

Składnia

import { memo } from 'react';

const MemoizedComponent = memo(SomeComponent, arePropsEqual?);
  • SomeComponent to dowolny komponent funkcyjny. Zwykle owija się bezpośrednio definicję: const Card = memo(function Card(props) { ... }). Nazwanie wewnętrznej funkcji zachowuje nazwę w React DevTools i w komunikatach o błędach.
  • arePropsEqual jest opcjonalne i rzadko potrzebne (zobacz niżej).
  • Wynik to komponent, który renderujesz jak każdy inny.

Domyślnie React porównuje stare i nowe propsy płytko: sprawdza każdy prop przez Object.is. Teksty, liczby i wartości logiczne porównuje się po wartości, więc title="Hi" równa się title="Hi". Obiekty, tablice i funkcje porównuje się po referencji: dwa obiekty z tą samą zawartością to wciąż różne obiekty.

W przykładzie powyżej items jest zadeklarowane poza komponentem, więc przy każdym renderowaniu to ta sama tablica, a porównanie przechodzi.

Nowe obiekty i funkcje unieważniają memo

Wszystko zapisane jako literał w ciele rodzica jest tworzone na nowo przy każdym renderowaniu: { name: 'Ada' }, [1, 2, 3], () => save(). Przekaż coś takiego do komponentu z memo, a prop nigdy nie będzie równy poprzedniemu.

Każde kliknięcie wypisuje render Badge object prop, ale nigdy wersji z tekstem. Są trzy sposoby na naprawę przypadku z obiektem:

  • Przekazuj wartości prymitywne. name="Ada" zamiast user={{ name: 'Ada' }}, gdy dziecko potrzebuje tylko jednego czy dwóch pól.
  • Przenieś stałe poza komponent. Obiekt, który nigdy się nie zmienia, może żyć na poziomie modułu, jak items w pierwszym przykładzie.
  • Zapamiętaj go. Owijaj obiekty i tablice zbudowane z propsów albo stanu w useMemo, a funkcje w useCallback.

children ma ten sam problem. <Card><p>Hello</p></Card> tworzy nowy obiekt elementu <p> przy każdym renderowaniu, więc Card z memo, który dostaje children, renderuje się za każdym razem.

Czego memo nie zatrzymuje

memo patrzy tylko na propsy przychodzące od rodzica. Zmemoizowany komponent nadal się renderuje, gdy:

  • zmienia się jego własny stan, przez jego własne useState albo useReducer;
  • zmienia się kontekst, który czyta, przez useContext.

Kliknij Unrelated, a Panel zostanie pominięty. Zmień motyw, a się wyrenderuje, bo czyta kontekst. Kliknij Like, a się wyrenderuje, bo zmienił się jego własny stan. To zamierzone zachowanie: memo służy do pomijania pracy, gdy nic, od czego zależy komponent, się nie zmieniło, a kontekst i stan to rzeczy, od których zależy.

Własna funkcja porównująca

Drugi argument zastępuje płytkie porównanie. Dostaje poprzednie i następne propsy i zwraca true, gdy mają być traktowane jako równe, czyli gdy React ma pominąć renderowanie.

const MemoChart = memo(Chart, (prev, next) => {
    return (
        prev.points.length === next.points.length &&
        prev.points.every((p, i) => p === next.points[i]) &&
        prev.color === next.color &&
        prev.onSelect === next.onSelect
    );
});

Rzadko jest potrzebna i łatwo się w niej pomylić. Jeśli funkcja ignoruje jakiś prop, komponent dalej pokazuje jego starą wartość. Najgorszy przypadek to ignorowanie propsa z funkcją: pominięty komponent dalej wywołuje starą funkcję, która czyta stary stan. Porównuj każdy prop i nie rób głębokich porównań na dużych danych, bo wtedy porównanie kosztuje więcej niż renderowanie, które oszczędza. Stabilne propsy przez useMemo i useCallback to prawie zawsze lepsze rozwiązanie.

Sprawdzanie, czy memo działa

Najszybsze sprawdzenie to to, którego używa ta strona: console.log na początku komponentu. Jeśli wypisuje się wtedy, gdy oczekiwałeś pominięcia, wypisz też propsy i porównaj je z poprzednimi, żeby znaleźć ten, który się zmienił. W prawdziwym projekcie dodatkowo pomaga React DevTools: włącz w jego ustawieniach "Highlight updates when components render", żeby zobaczyć, które komponenty mrugają przy każdej interakcji, a karta Profiler zapisuje każde renderowanie i, przy włączonym odpowiednim ustawieniu, powód, dla którego każdy komponent się wyrenderował.

Dwie rzeczy wyglądają jak awaria memo, ale nią nie są:

  • Pierwsze renderowanie zawsze następuje. memo pomija renderowania dopiero po tym, jak komponent raz wyrenderował się z tymi propsami.
  • Buildy deweloperskie mogą renderować więcej. Pod StrictMode React w trybie deweloperskim renderuje każdy komponent dwa razy, żeby wychwycić nieczysty kod. Podgląd tutaj działa jak build produkcyjny, więc każda linia logu powyżej oznacza jedno prawdziwe renderowanie.

Kiedy używać memo

memo się opłaca, gdy komponent renderuje się wolno, a jego rodzic często renderuje się z tymi samymi propsami dla niego: długa lista albo wykres obok pola tekstowego, panel boczny na stronie, która aktualizuje się co sekundę. Dla komponentów, które renderują się szybko albo których propsy i tak zmieniają się za każdym razem, dodaje porównanie i nic nie daje. Zanim zastosujesz memoizację, sprawdź, czy nie możesz zmienić struktury, przenosząc stan w dół albo przekazując JSX jako children, jak pokazuje strona o renderowaniu.

React Compiler 1.0, jeśli twój projekt go używa, automatycznie memoizuje komponenty i ich propsy w czasie budowania, przez co większość ręcznych wywołań memo staje się zbędna. Bez niego memo działa dokładnie tak, jak tu opisano, i pozostaje powszechne w kodzie w React.

Najczęściej zadawane pytania

Co robi React.memo?

Zwraca zmemoizowaną wersję komponentu. Gdy rodzic się renderuje, React porównuje nowe propsy z poprzednimi i pomija renderowanie komponentu, jeśli każdy prop jest równy według Object.is.

Dlaczego mój komponent z memo nadal się renderuje?

Zwykle dlatego, że prop to nowy obiekt, tablica albo funkcja utworzona podczas renderowania rodzica, więc nigdy nie jest równy poprzedniemu. Komponent renderuje się też, gdy zmienia się jego własny stan albo kontekst, który czyta, z memo czy bez.

Czym różni się React.memo od useMemo?

memo owija komponent i pomija jego renderowanie. useMemo to hook wewnątrz komponentu, który zapamiętuje wyliczoną wartość. Często działają razem: useMemo utrzymuje stabilny prop z obiektem, żeby memo mogło pominąć renderowanie.

Czy owijać każdy komponent w memo?

Nie. Większość renderowań jest tania, a porównanie propsów ma własny koszt. Używaj go dla komponentów, które renderują się wolno i często dostają te same propsy.

Czy memo działa z children?

Rzadko. JSX przekazany jako children to nowy obiekt przy każdym renderowaniu, więc komponent z memo, który dostaje children, prawie zawsze renderuje się ponownie.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ