Strona główna / Artykuły / Jak przeglądarka rysuje obraz i jakie ma znaczenie React w tym procesie

Jak przeglądarka rysuje obraz i jakie ma znaczenie React w tym procesie

Dowiedz się, jak Critical Rendering Path, reconciliation, Fiber oraz Scheduler współpracują ze sobą, aby przekształcać aktualizacje React w piksele na ekranie.

1798 słów

Najpierw zapomnij o React: jak przeglądarka faktycznie rysuje stronę?

Odstaw na chwilę React. Nawet prosty dokument HTML z odrobiną CSS przechodzi przez określoną sekwencję kroków, zanim pojawi się choć jeden piksel. Każda przeglądarka postępuje zgodnie z tą sekwencją, niezależnie od narzędzi użytych do stworzenia strony:

HTML → DOM tree
CSS  → CSSOM tree
DOM + CSSOM → Render Tree
Render Tree → Layout → Paint → Composite → Screen

Każdy etap pełni konkretne zadanie, a same nazwy nie ujawniają tego od razu:

  • Drzewo DOM — przeglądarka analizuje Twój HTML i przekształca go w drzewo węzłów. To czysta struktura: które elementy znajdują się wewnątrz których.
  • Drzewo CSSOM — ta sama zasada dotyczy stylów. Każda zapisana przez Ciebie reguła CSS jest przekształcana w drzewo, które przeglądarka może wykorzystać do wyszukiwania informacji.
  • Drzewo renderowania — przeglądarka łączy te dwa drzewa: przechodzi przez DOM, do każdego węzła przypisuje odpowiadające mu reguły CSSOM i pomija wszystko, co faktycznie nie będzie widoczne (element z display: none nadal istnieje w DOM, ale jest wykluczony z drzewa renderowania).
  • Rozmieszczenie — to etap arytmetyczny. Biorąc pod uwagę każdy element oraz jego obliczone style, przeglądarka określa dokładną pozycję i rozmiar każdego z nich.
  • Rysowanie — teraz przeglądarka wypełnia elementy wizualne: tekst, kolory tła, obramowania, cienie itp.
  • Kompozycja — gdy strona ma kilka warstw (co często zdarza się ze względów wydajnościowych), przeglądarka układa je we właściwej kolejności, a ten ostateczny wynik jest tym, co faktycznie trafia na twoją ekran.
  • Wszystkie te kroki razem tworzą to, co nazywane jest Krytyczną ścieżką renderowania, czyli CRP.

    Ta sekwencja działa identycznie niezależnie od tego, czy używasz React, Vue, zwykłego jQuery, czy w ogóle żadnej biblioteki. Rysowanie pikseli to odpowiedzialność przeglądarki, a nie coś, co przejmuje jakakolwiek ramka interfejsu użytkownika.

    A więc jakie jest miejsce React w tym wszystkim?

    To właśnie ta część wymaga chwili na kliknięcie. React nie zastępuje Krytycznej ścieżki renderowania — działa przed nią.

    Bez React aktualizacja interfejsu oznacza ręczne znajdowanie odpowiedniego węzła DOM i jego modyfikację:

    const counterEl = document.getElementById('counter');
    counterEl.textContent = newCount;
    

    To jest do zarządzania w przypadku pojedynczego licznika. Ale wyobraź sobie panel sterowania, gdzie czterdzieści oddzielnych wartości może się zmieniać niezależnie — musiałbyś ręcznie śledzić i aktualizować każdą z nich. Rozwiązywanie właśnie tego problemu jest powodem istnienia React.

    Z Reactem ta sama aktualizacja wygląda w ten sposób:

    function Counter({ count }) {
      return <p>{count}</p>;
    }
    

    Opisujesz, jak powinien wyglądać interfejs na podstawie obecnych danych, i nigdy nie manipulujesz bezpośrednio DOM-em. Dlatego ktoś inny musi wykonać tę pracę. To właśnie jest zadaniem Reacta, które odbywa się na etapie przed rozpoczęciem procesu krytycznego renderowania przeglądarki:

    State changes → React figures out what changed → applies a small patch to the real DOM
                                                                  ↓
                              browser does its normal thing: Layout → Paint → Composite → Screen
    

    Cała wartość oferowana przez React polega na tym, by ten pierwszy krok – określenie tego, co się zmieniło – był jak najszybszy i dokładniejszy, dzięki czemu przeglądarka musi ponownie przetworzyć tylko tę małą część strony, która tego wymaga, zamiast przetwarzać wszystko od nowa.

    Programowanie imperatywne versus deklaratywne: zmiana sposobu myślenia

    To przeciwieństwo wyjaśnia, dlaczego React został zaprojektowany w taki sposób.

    Kod imperatywny opisuje każdy pojedynczy krok:

    list.innerHTML = '';
    for (const item of items) {
      const li = document.createElement('li');
      li.textContent = item;
      list.appendChild(li);
    }
    

    To ty decydujesz: usuń to, stwórz ten element i przymocuj go tutaj.

    Kod deklaratywny zamiast tego opisuje wynik, który chcesz zobaczyć:

    <ul>
      {items.map(item => <li key={item}>{item}</li>)}
    </ul>
    

    Zamiast polecać przeglądarce „stworzyć li i wstawić go”, określasz „biorąc pod uwagę ten tablicę, oto jak powinna wyglądać końcowa interfejs użytkownika”. Coś innego musi przekształcić ten opis w konkretne operacje DOM – a zrozumienie, czym jest to coś, jest krokiem następnym.

    Czym naprawdę jest „rozrachunek”

    To zagadnienie wydaje się bardziej skomplikowane, niż w rzeczywistości jest, gdy spojrzy się na nie bezpośrednio.

    React przechowuje mentalną kopię Twojej interfejsu użytkownika, zwaną Virtual DOM. Gdy coś się zmienia, React tworzy nową wersję tego drzewa i porównuje ją z poprzednią, aby ustalić, co się różni. To kroku porównania ludzie nazywają reconciliation.

    Sprawdzanie każdej możliwej różnicy pomiędzy dwoma drzewami byłoby obliczeniowo skomplikowane, dlatego React stosuje skrót poprzez dwie zasady, które zapewniają szybką pracę w praktyce:

    1. Typ elementu się zmienił (na przykład `

    ` zamienił się w ``) — React nawet nie sprawdza dziecięcych elementów; całkowicie odrzuca stary węzeł i tworzy nowy.

    1. Typ elementu pozostał taki sam (`

    staje się

    `) — React zachowuje istniejący węzeł DOM i jedynie naprawia te części, które się różnią.

    Jedna z pułapek, która dotyka niemal wszystkich, dotyczy list. Domyślnie React porównuje elementy listy według ich indeksu – element 0 z elementem 0, element 1 z elementem 1 i tak dalej. Jeśli wstawisz nowy wpis na początku listy bez klucza, React zakłada, że wszystkie elementy poniżej niego również uległy zmianie.

    Dlatego zawsze należy przypisać stabilny key elementom listy:

    {items.map(item => <li key={item.id}>{item.name}</li>)}
    

    Gdy elementy mają klucz, React może rozpoznać, „ten konkretny element zmienił pozycję”, zamiast zakładać, że cała lista została odbudowana.

    Po tych wszystkich porównaniach React otrzymuje krótki, precyzyjny zestaw instrukcji – takich jak „aktualizuj ten węzeł tekstu” lub „wstaw węzeł tutaj” – i to właśnie te operacje są faktycznie stosowane w rzeczywistym DOM-ie.

    Czy to nie jest to samo, co robi Fiber?

    To słuszne pytanie, nad którym warto się zatrzymać.

    Podstawową ideą jest pojednanie, a Fiber to po prostu narzędzie służące do jego realizacji.

    Przed React 16 tym narzędziem nazywano Stack Reconciler. Przechodził on rekurencyjnie i synchronicznie przez całe drzewo, co oznaczało, że gdy już się rozpoczął, musiał zostać dokończony przed zatrzymaniem. W przypadku dużych aktualizacji mogło to zablokować główny wątek na tyle długo, że aplikacja działała wolno – spadało liczba klatek oraz wpisywanie tekstu stawało się nieresponsywne.

    Fiber, wprowadzony w React 16, zastąpił ten mechanizm. Koncepcja synchronizacji nie uległa zmianie, ale teraz praca jest dzielona na małe fragmenty, które można wstrzymać, odrzucić lub wznowić później. Jeśli pojawi się coś pilniejszego – na przykład wpisywanie tekstu przez użytkownika – React może przerwać jakąkolwiek pracę o niższej priorytecie, zająć się pilną aktualizacją, a następnie wrócić do punktu, w którym przerwał.

    Dlatego nie jest dokładne stwierdzenie, że Fiber zastąpił synchronizację. Dokładniej jest powiedzieć, że starszy silnik realizujący synchronizację został zastąpiony bardziej wydajnym.

    Jakie więc zadanie pełni Scheduler?

    Fiber umożliwia wstrzymywanie i wznowianie pracy, ale coś innego musi zdecydować kiedy ją wstrzymać i które zadanie zasługuje na priorytet. To właśnie jest rolą Schedulera.

    Jego obowiązki obejmują:

    • Aktualizacje rankingu według pilności — na przykład traktowanie naciśnięcia klawisza w polu wprowadzania jako pilnego, podczas gdy odświeżanie dalekiej listy w tle nie jest takie pilne.
    • Zapełnianie luk pomiędzy ramkami przeglądarki w celu postępuwania krok po kroku przy zadaniach o niższej priorytecie oraz cofanie się przed koniecznością renderowania kolejnej ramki.
    • Włączanie funkcji React 18, takich jak startTransition, gdzie oznaczenie aktualizacji jako nienaglądnej faktycznie informuje Scheduler, że może przesunąć to zadanie na dalszy koniec listy priorytetów.

    Prosty model mentalny łączy te trzy koncepcje:

    Reconciliation  → the algorithm (what changed?)
    Fiber           → the engine that makes that algorithm interruptible
    Scheduler       → the traffic controller deciding when to pause/resume Fiber's work
    

    Faza renderowania versus faza zatwierdzenia

    Istnieje jeszcze jedna różnica, którą warto zrozumieć: Fiber dzieli swoją pracę na dwie fazy, które podlegają zupełnie innym zasadom.

    Faza renderowania to moment, w którym odbywa się faktyczne porównywanie zmian. React wywołuje funkcje komponentu, buduje nową strukturę drzewa i porównuje ją z poprzednią. Nic z tego jeszcze nie wpływa na rzeczywistą stronę, dlatego tę fazę można bezpiecznie wstrzymać, pominąć lub rozpocząć od nowa.

    Faza zatwierdzania to moment, w którym React ostatecznie zapisuje zmiany do rzeczywistego DOM-u i aplikuje obliczony patch. Tę fazę nie można przerwać – przebiega ona od początku do końca w jednej, nieprzerwanej sekwencji, ponieważ częściowa aktualizacja interfejsu pozostawiłaby stronę w uszkodzonym stanie wizualnym. Bezpośrednio po aktualizacji DOM-u, ale przed tym, jak przeglądarka narysuje ekran, useLayoutEffect jest wywoływany synchronicznie. useEffect, natomiast, uruchamia się nieco później, po tym jak przeglądarka już zakończyła rysowanie.

    Czy naprawdę potrzebujesz Reacta?

    Szczerze mówiąc, nie zawsze. Wiele stron produkcyjnych działa wyłącznie na bazie HTML, CSS i zwykłego JavaScriptu i funkcjonuje bez problemów.

    React zaczyna usprawiedliwiać swoje dodatkowe obciążenie, gdy wymagania stają się bardziej złożone:

    • Ręczne zarządzanie aktualizacjami DOM jest do przyjęcia w małych projektach, ale staje się niemożliwe, gdy musimy obsługiwać dziesiątki wzajemnie powiązanych elementów interfejsu.
    • Znaczna część błędów w rzeczywistych interfejsach pochodzi z niesynchronizacji stanu z wyświetlanym interfejsem. Podejście Reacta — traktowanie interfejsu jako funkcji stanu i pozostawienie ramworkowi zadania porównywania zmian — eliminuje ten problem już na poziomie projektu.
    • Możliwość tworzenia komponentów wielokrotnego użycia, wspierana ekosystemem narzędzi do routingu, narzędzi deweloperskich oraz wspólnych konwencji, staje się cenna, gdy nad tym samym kodem pracuje więcej niż jeden programista.

    Dla prostej strony startowej lub w dużej mierze statycznej witryny zwykły JavaScript jest lepszym wyborem. Wprowadzanie Fiber, Schedulera oraz pełnego procesu synchronizacji oznaczałoby ponoszenie dodatkowych kosztów za problem, którego tak naprawdę nie mieliśmy.

    React nie jest z natury lepszy od JavaScriptu. To zestaw narzędzi stworzony do rozwiązywania jednego konkretnego problemu: utrzymywania synchronizacji interfejsu użytkownika z stanem, który ciągle się zmienia, na dużą skalę i w ramach zespołu. Poniżej tej skali zwykły HTML, CSS i JS doskonale radzą sobie z tym zadaniem.

    Literatura pokrewna

  • Unikanie błędów cichego stanu spowodowanych mutacją referencji w JavaScript — Dowiedz się, dlaczego modyfikowanie obiektów i tablic poprzez referencje zakłóca ponowne renderowanie w React, dlaczego operacja spread kopiuje tylko powierzchownie oraz jak bezpiecznie stworzyć głęboką kopię stanu.
  • Praktyczne porównanie wzorców struktury folderów w React — Wyjaśnia struktury projektów React oparte na funkcjach, warstwach i domenie oraz daje wskazówki dotyczące wyboru odpowiedniej struktury w miarę rozwoju aplikacji.