Menu

Renderowanie w React: co wywołuje ponowne renderowanie

Komponent Reacta renderuje się ponownie, gdy zmienia się jego stan, gdy renderuje się jego rodzic albo gdy zmienia się kontekst, który czyta. Poznaj różnicę między renderowaniem a zatwierdzeniem (commit), dowiedz się, co naprawdę oznacza wirtualny DOM i jak zatrzymać niepotrzebne renderowania.

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

Komponent Reacta renderuje się ponownie, gdy zmienia się jego własny stan, gdy renderuje się ponownie jego rodzic albo gdy zmienia się kontekst, który czyta. Renderowanie oznacza, że React znowu wywołuje funkcję twojego komponentu, żeby dostać świeży JSX; potem aktualizuje tylko te części strony, które faktycznie się zmieniły.

Otwórz konsolę pod podglądem i kliknij przycisk. Wszystkie trzy komponenty wypisują log, także Title, który w ogóle nie przyjmuje propsów. Wyrenderował się, bo wyrenderował się jego rodzic.

Co wywołuje renderowanie

React renderuje komponent dokładnie z tych powodów:

  1. Pierwsze renderowanie. Aplikacja startuje albo komponent po raz pierwszy pojawia się w drzewie.
  2. Zmienił się jego stan. Wywołałeś setter z useState albo dispatch z useReducer z nową wartością.
  3. Wyrenderował się jego rodzic. Domyślnie, gdy komponent się renderuje, każdy komponent, który zwraca, też się renderuje, aż do samego dołu.
  4. Zmienił się kontekst, który czyta. Komponent, który wywołuje useContext(SomeContext), renderuje się, gdy najbliższy provider przekaże nowe value (zobacz useContext).

Popularne przekonanie mówi, że komponent renderuje się, „bo zmieniły się jego propsy". To odwraca przyczynę. Propsy to argumenty, które rodzic przekazuje podczas renderowania, więc nowe propsy mogą przyjść tylko wtedy, gdy rodzic się renderuje. A samo renderowanie rodzica wystarcza: Title powyżej nie ma propsów, a i tak renderuje się za każdym razem.

Ustawienie stanu na wartość, którą już ma (porównaną przez Object.is), nie uruchamia renderowania dzieci. React może jeszcze raz wywołać ten jeden komponent, zanim zauważy, że nic się nie zmieniło, ale wynik wyrzuca.

Renderowanie i zatwierdzenie

Każda aktualizacja przechodzi przez dwie fazy:

  • Renderowanie (render). React wywołuje twoje komponenty. Zwracają JSX, czyli po prostu obiekty opisujące, co ma pokazywać ekran. Nic na stronie się jeszcze nie zmienia, dlatego renderowanie musi być czyste: żadnych zapisów do DOM, żadnych żądań, żadnych zmian zmiennych poza komponentem.
  • Zatwierdzenie (commit). React porównuje nowy wynik z poprzednim i stosuje różnice w DOM: wstawia, usuwa albo aktualizuje tylko te węzły, które się zmieniły. Potem przeglądarka maluje ekran, a następnie React uruchamia twoje efekty.

Przykład poniżej renderuje się przy każdym kliknięciu, ale React zachowuje ten sam element <input>. Efekt uruchamia się po każdym zatwierdzeniu i to sprawdza.

Wpisz coś w pole i kliknij kilka razy. Wpisany tekst zostaje, bo React nigdy nie zastąpił pola: jedyna zmiana DOM przy każdym kliknięciu to liczba wewnątrz <p>. Log pokazuje też kolejność: najpierw render, a efekt po zatwierdzeniu.

Wirtualny DOM po prostu

„Wirtualny DOM" to popularna nazwa obiektów, które zwracają twoje komponenty. <p>Items: {count}</p> kompiluje się do wywołania, które tworzy obiekt w rodzaju { type: 'p', props: { children: ['Items: ', 1] } }. Po renderowaniu React przechodzi po nowym drzewie tych obiektów obok poprzedniego, a tam, gdzie typ i pozycja się zgadzają, zachowuje istniejący węzeł DOM i aktualizuje tylko zmienione atrybuty i tekst. To porównanie nazywa się rekoncyliacją (reconciliation).

Ten termin jest nieprecyzyjny z dwóch powodów. React nie trzyma drugiej kopii DOM i nie porównuje dwóch DOM: porównuje obiekty elementów z własnym wewnętrznym drzewem komponentów (drzewem fiber). A ten sam proces napędza cele, które w ogóle nie mają DOM, takie jak React Native. Dokumentacja Reacta zwykle unika tego wyrażenia i mówi zamiast tego o renderowaniu i zatwierdzaniu. W praktyce liczy się konsekwencja: renderowanie jest tanie w porównaniu z pracą na DOM, bo większość renderowań kończy się małą aktualizacją DOM albo żadną.

Z tego, jak działa porównanie, wynikają dwie zasady. Inny typ elementu w tym samym miejscu (<div> zastąpiony przez <section> albo ComponentA przez ComponentB) niszczy stare poddrzewo i jego stan. A w listach key mówi Reactowi, który element jest którym, więc może przesuwać węzły, zamiast budować je od nowa.

Zatrzymywanie niepotrzebnych renderowań

Większość dodatkowych renderowań nie kosztuje niczego zauważalnego. Gdy któreś naprawdę szkodzi, na przykład duża lista renderuje się przy każdym naciśnięciu klawisza w innym miejscu strony, wypróbuj te sposoby w tej kolejności.

Przenieś stan w dół

Jeśli tylko jedna mała część ekranu używa kawałka stanu, trzymaj ten stan w komponencie, który owija tylko tę część. Ta wersja ponownie renderuje ProductList przy każdym naciśnięciu klawisza:

export default function App() {
    const [text, setText] = useState('');
    return (
        <>
            <input value={text} onChange={(e) => setText(e.target.value)} />
            <ProductList />
        </>
    );
}

Przenieś pole i jego stan do osobnego komponentu, a lista przestanie być wewnątrz komponentu, który się renderuje:

Wpisz kilka liter: log wypisuje tylko SearchBox.

Przekaż children

Czasem stan musi żyć w opakowaniu wokół kosztownej części, na przykład w panelu, który przełącza wyróżnienie. Niech opakowanie przyjmuje children. Rodzic tworzy elementy dzieci, a ponieważ rodzic nie renderuje się, gdy zmienia się stan opakowania, te elementy są tymi samymi obiektami co wcześniej, więc React je pomija.

Kliknij przełącznik, a log wypisze tylko Highlighter. Teraz przenieś <Article /> do JSX komponentu Highlighter w miejsce {children}, a każde kliknięcie wypisze też render Article.

Memoizuj

Gdy żadna zmiana struktury nie pasuje, owiń dziecko w memo. React porównuje wtedy jego propsy z poprzednimi i pomija renderowanie, gdy wszystkie są takie same. Obiekty i funkcje tworzone podczas renderowania są za każdym razem nowe, więc memo zwykle idzie w parze z useMemo albo useCallback dla tych propsów. Strona o React.memo pokazuje to na działających przykładach.

import { memo } from 'react';

const ProductList = memo(function ProductList({ category }) {
    // skipped while category stays the same
});

React Compiler może dodać taką memoizację za ciebie w czasie budowania, ale zmiana struktury stanu to wciąż pierwsza rzecz do wypróbowania: usuwa pracę, zamiast ją cache'ować.

Renderowania zwykle są w porządku

Renderowanie to wywołanie funkcji, które zwraca obiekty. React szybko wykonuje ich tysiące, a renderowanie, które daje ten sam wynik, nie zmienia DOM. Nie dodawaj memo wszędzie „na wszelki wypadek": każde porównanie ma własny koszt i utrudnia czytanie kodu. Najpierw mierz. Profiler w React DevTools pokazuje, które komponenty się wyrenderowały, dlaczego i ile każde trwało.

Najczęściej zadawane pytania

Co powoduje ponowne renderowanie komponentu Reacta?

Trzy rzeczy: zmienia się jego własny stan, renderuje się jego rodzic albo kontekst, który czyta przez useContext, dostaje nową wartość. Propsy nie są osobnym wyzwalaczem: nowe propsy przychodzą tylko dlatego, że rodzic się wyrenderował.

Czy dziecko renderuje się ponownie, gdy renderuje się rodzic?

Tak, domyślnie każdy komponent wewnątrz renderującego się rodzica też się renderuje, nawet jeśli jego propsy się nie zmieniły. Owinięcie dziecka w memo pozwala Reactowi je pominąć, gdy jego propsy są takie same jak ostatnio.

Co to jest wirtualny DOM w React?

To luźna nazwa drzewa zwykłych obiektów JavaScript (elementów Reacta), które zwracają twoje komponenty. React porównuje nowe drzewo z poprzednim i zmienia tylko te prawdziwe węzły DOM, które się różnią.

Czy ponowne renderowanie szkodzi wydajności?

Zwykle nie. Renderowanie to wywołanie funkcji, które tworzy obiekty, a React dotyka DOM tylko tam, gdzie zmienił się wynik. Optymalizuj, gdy renderowanie jest mierzalnie wolne, na przykład według Profilera w React DevTools.

Czym różni się renderowanie od zatwierdzenia (commit)?

Renderowanie to wywołanie przez React twoich komponentów, żeby dowiedzieć się, jak ma wyglądać ekran. Zatwierdzenie to zastosowanie przez React różnic w DOM. Renderowanie, które daje ten sam wynik, nie zatwierdza żadnych zmian w DOM.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ