Diffing Algorytm
Dzisiejszym bohaterem jest Diffing algorytm, dzięki któremu React osiąga wysoką szybkość i efektywność. Korzystamy z niego codziennie, często nie zdając sobie z tego sprawy. Warto zajrzeć pod jego 'maskę', by jeszcze lepiej wykorzystywać potencjał algorytmu.
Czym jest Diffing algorytm
Jest to podstawowy mechanizm wykorzystywany przez bibliotekę React do wykrywania oraz wdrażania zmian na naszym UI. Można go również porównać do detektywa, który w gąszczu zmian musi znaleźć tylko te kluczowe i wydajnie wprowadzić je w życie.
Nie możemy jednak przejść do sedna, bez wyjaśnienia kluczowego pojęcia związanego z opisywanym algorytmem. Chodzi oczywiście o Virtual DOM.
Virtual Dom
Zapewne wielokrotnie zdarzyło Ci się otwierać Dev tools i zmieniać elementy HTML ręcznie lub za pośrednictwem JavaScript dodawać, usuwać i modyfikować węzły. Podczas takich operacji działasz na DOM (Document Object Model), czyli drzewiastej strukturze odzwierciedlającej prawdziwego HTML’a.
Niestety, każde takie działanie negatywnie wpływa na wydajność. Każda modyfikacja jest ciężką operacją. Jeżeli będziemy z nich często korzystać, wpłynie to negatywnie na działanie naszej aplikacji.
Na ratunek przychodzi Virtual DOM, czyli lekka wirtualna kopia rzeczywistego DOM’a. Możemy wprowadzać nasze zmiany bez obaw, gdyż to, co w nim zrobimy, nie ma od razu przełożenia na to, co widzimy na ekranie. Wszystko jest przechowywane w pamięci
Znając już różnice między DOM’ami, wróćmy do tytułowego bohatera naszego wpisu. Cała machina uruchamia się, gdy następuje render komponentu. Podczas pierwszego sprawa jest prosta, ale schody zaczynają się podczas ponownych rerenderów. Diffing algorytm działa wtedy dwukrotnie. Pierwszy raz, aby porównać realny DOM z virtual DOM’em. Następnie po to, aby nanieść zmiany wirtualne do rzeczywistego HTML’a.
Proces porównywania DOM’ów nosi nazwę reconciliation i najnowocześniejsze algorytmy dyfuzji drzewa mają złożoność obliczeniową O(n³). Brzmi groźnie, ale w praktyce oznacza to, że gdybyśmy porównywali 100 elementów w strukturach drzewiastych, potrzebne by było około miliona porównań.
Skoro najlepsze algorytmy są aż tak wolne, to pewnie zastanawiasz się, jakim cudem React jest tak szybki. Odpowiedź znajdziesz w następnym akapicie.
Działanie Diffing algorytmu krok po kroku
W celu osiągnięcia jak najlepszej wydajności proces reconciliation stosuje pewne heurystyki. Są to założenia upraszczające i brzmią następująco:
- Jeżeli element zawierający się zmienił, to cała jego zawartość również się zmieniła
- Elementy z tym samym atrybutem key są niezmienne we wszystkich renderach
Dzięki temu React nie musi wykonywać aż takiej liczby porównań i operacji, a jego efektywność wcale na tym nie cierpi.
Przyjrzyjmy się teraz z bliska całemu procesowi krok po kroku:
- Wirtualna reprezentacja DOM: Najpierw tworzona jest wirtualna reprezentacja DOM, znana jako Virtual DOM. Gdy dane ulegają zmianie nowy VDOM zostanie utworzony
- Różnicowanie elementów: algorytm reconciliation przeprowadza proces różnicowania, porównując poprzednie drzewo VDOM z nowym. Identyfikuje różnice między dwoma drzewami w pamięci.
- Minimalne aktualizacje: Zamiast aktualizować cały DOM, React generuje minimalny zestaw aktualizacji, potrzebnych do przekształcenia bieżącego DOM w celu dopasowania go do najnowszego VDOM.
- Grupowanie aktualizacji: Aby uniknąć nadmiernych manipulacji, React grupuje aktualizacje i stosuje je w pojedynczej partii. Zmniejsza to liczbę potrzebnych "namalowań UI" przez przeglądarki i poprawia wydajność. Technika grupowania często występuje pod nazwą batching
Przykłady Heurystyk w praktyce
Kroki algorytmu już znamy, a teraz rzućmy okiem na kilka typowych sytuacji w oparciu o heurystyki algorytmu:
- Elementy tego samego typu: Załóżmy, że mamy dwa paragrafy, ale z różnym tekstem. React skupi się na zamianie tekstu i atrybutów, ale samego paragrafu nie wyrzuca.
- Elementy różnego typu: Gdy zamieniamy np. div na span, algorytm usunie cały div wraz z zawartością i w jego miejscu utworzy nowy span.
- Klucze dla list elementów: Przy operacjach na listach, które posiadają unikalny atrybut key, nawet przy zmianie kolejności, usuwaniu czy dodawaniu elementów, nie ma konieczności tworzenia całej listy od nowa.
Jak optymalizować nasz kod w React
Zachowując pragmatyzm, wskażemy dzisiaj dwa bardzo istotne usprawnienia, które przy niewielkim zaangażowaniu znacząco poprawią wydajność aplikacji:
- Zdecydowanie warto dodawać unikalne atrybuty key podczas renderowań list elementów. Wesprzemy wtedy algorytm w identyfikacji zmian. Gdy następnym razem w kodzie zauważysz pętlę zwracającą JSX, pamiętaj o dodaniu unikalnego key.
- Unikaj zbędnych renderowań przy użyciu pure components lub hooków takich jak useMemo czy useCallback (temat na osobny wpis). Jest to zwłaszcza istotne przy komponentach na górze struktury, gdyż ich ponowny render uruchomi całą kaskadę renderów dzieci.
Podsumowanie
Po przeczytaniu tego wpisu już wiesz, dlaczego React działa tak szybko, nawet przy częstych i znacznych zmianach w interfejsach użytkownika. Dodatkowo dysponujesz już wiedzą, jak skutecznie wesprzeć proces reconciliation. Jeśli zastosujesz nasze porady, diffing algorytm będzie Ci wdzięczny, a Twoja aplikacja zyska na szybkości.