Angular vs React. Która z technologii jest wydajniejsza?

W Synergy Codes od (już prawie) dekady zajmujemy się wizualizacją danych. Naszym głównym orężem jest GoJS i przez większość tego czasu łączyliśmy go z Reactem, jednak na przestrzeni ostatniego roku znacząco wzrosła liczba zapytań o projekty angularowe. Szybko dostosowaliśmy się do rynku i opanowaliśmy połączenie GoJS + Angular. Do tej pory dobór partnera dla biblioteki diagramowej zależał głównie od istniejącej infrastruktury klienta lub jego wymagań.

Kacper Cierzniewski. Lider zespołu Uranus i Software Developer w Synergy Codes. Główny obszar zainteresowań to frontend, w szczególności biblioteka React. W wolnych chwilach rozwija się w technologiach backendowych (głównie nodejs). Miał przelotny romans z Unity i tworzeniem aplikacji na VR. Poza programowaniem poświęca czas na granie na instrumentach (gitara i pianino) i mozolną naukę teorii muzyki. Lubi wyzwania i miejsca, gdzie można wykazać się kreatywnością.

Dawid Perdek. Programista Frontend w Synergy Codes. Większość czasu pracował z Angularem, ale realizował też projekty z wykorzystaniem Reacta i Electrona. Poza pisaniem kodu prowadzi Gildię Software Delivery, w ramach której badane i udoskonalane są Firmowe procesy dotyczące rozwijania oprogramowania. Inicjator, lubi pracę z ludźmi, chętnie dzieli się wiedzą, czy to w Firmie, czy poza nią. Ostatnio mocno rozwija się w kierunku modelowania i architektury oprogramowania.

Ostatnio jednak zdarzają nam się też zapytania o “gołe” projekty angularowe lub reactowe, od zera, bez GoJS, a to doprowadziło nas do rozważań na temat wyższości jednego nad drugim. Jedno z istotnych kryteriów to na pewno wydajność i to nad tym tematem chcielibyśmy się dzisiaj pochylić.

Założenia testu

Postanowiliśmy w jakiś sposób zbadać ten aspekt i wyciągnąć własne wnioski. Do wykonania miarodajnego testu potrzebowaliśmy przyjąć pewne założenia. Najważniejsze było wykonywanie testów na buildach produkcyjnych – nie chcemy, żeby udogodnienia związane z trybem deweloperskim wpływały na czas budowania, rozmiar paczek i ogólne działanie aplikacji. Obie aplikacje powinny też mieć analogiczne drzewa komponentów, taką samą strukturę HTML wewnątrz oraz identyczne CSS-y. 

Aplikacje muszą też zawierać listę ze sporą liczbą elementów, wyniki zbadamy dla kilku różnych rzędów wielkości tej liczby, a dla każdego z tych rzędów test powtórzymy dziesięć razy i wyciągniemy średnie wartości. Testować będziemy tylko na najświeższej wersji przeglądarki Google Chrome, każdy na swoim komputerze, ale będziemy mieli te same wersje Node’a.

Wybieranie sposobu pomiaru

Temat nie jest prosty, ponieważ chcemy miarodajnie porównać dwa różne światy – Reacta i Angulara. Światy te mają pewne wbudowane metody do pomiaru wydajności, lecz jeśli są to własne implementacje, to prawdopodobnie będą w różny sposób dokonywać pomiaru. Prowadzi to do konkluzji, iż uzyskane wyniki okażą się niemiarodajne. Innym sposobem byłby prosty console.log() umieszczony w odpowiednim miejscu w kodzie. Jednakże określenie tego “odpowiedniego miejsca” może być dość trudne ze względu na to, że będziemy mieli do czynienia z dwiema różnymi technologiami. 

Po przeprowadzonej dyskusji doszliśmy do wniosku, iż najlepszą metodą jest użycie narzędzia do mierzenia wydajności wbudowanego w przeglądarkę Google Chrome (zakładka Performance w narzędziach deweloperskich). Jest ono niezależne od technologii oraz mierzy czas wykonywanych funkcji javascryptowych. Może to być całkiem przydatne, aby porównać różne wbudowane podejścia frameworków. Uzyskane wyniki będą granularne, więc będzie można się przyjrzeć różnym aspektom i temu, jakie operacje zajmują najwięcej czasu. 

Narzędzie to również mierzy aktualne zużycie pamięci, co jest wartym uwagi dodatkiem, aby w praktyce zobaczyć różnice w zarządzaniu zasobami pomiędzy virtual DOM wykorzystanym w React’cie a Incremental DOM wykorzystanym w Angularze.

Podsumowując, wybór padł na Google Chrome i jego wbudowane narzędzie do mierzenia wydajności. Jako kryteria porównania przyjęliśmy czas trwania renderowania oraz zużycie pamięci RAM.

Przejdźmy zatem do zaprezentowania struktury naszej testowej aplikacji.

Opis testowej aplikacji

Aplikacja, na której będziemy testować wydajność Angulara/Reacta jest względnie prosta. Mamy tu do czynienia z podstawową strukturą aplikacji (header, content oraz footer). Aplikacja ta to wygenerowana lista pracowników firmy z przedstawieniem ich imienia, nazwiska, stanowiska oraz wieku. W footerze jesteśmy w stanie w prosty sposób zmanipulować dane, aby wymusić rerenderowanie, tzn. zmieniamy wszystkim pracownikom stanowisko na “backend developer”. Znajdziemy tu również możliwość zmiany ilości pracowników, co będzie kluczowe do tego, aby sprawdzić, jak wybrane technologie poradzą sobie z większą ilością danych. 

W dodatku oprócz samego przerenderowania mamy opcje dodania oraz usunięcia elementu z listy. 

Przewidywania

Aplikacje przygotowaliśmy w taki sposób, żeby miały jak najbardziej zbliżoną strukturę drzewa DOM. Używaliśmy podstawowych dostępnych metod, bez zewnętrznych mechanizmów zarządzania stanem typu Redux. W przypadku Reacta mamy więc zwykły “props-drilling”, aby przekazać zaktualizowany stan. Wiąże się to jednak z koniecznością odświeżenia praktycznie całej struktury aplikacji. W Angularze użyliśmy strategii detekcji zmian OnPush oraz w obu przypadkach nadaliśmy unikalne klucze elementom listy (key w React oraz trackBy w Angular). Spodziewamy się, że React poradzi sobie z zadaniem renderowania całego widoku szybciej, ale liczymy się z prawdopodobnie wyższym zużyciem pamięci. Wykorzystany w React Virtual DOM pozwala na bardzo szybką obsługę zmian, ale wymaga do tego wielu pełnych kopii drzewa DOM trzymanych w pamięci, potrzebny jest też mechanizm do bieżącego porównywania zmian między tymi wirtualnymi drzewami. Dzięki Incremental DOM Angular nie wymaga tak dużo pamięci, lecz w zamian traci na szybkości działania. Zakładamy też, że zbudowana aplikacja będzie dużo mniejsza w przypadku Reacta, jako że stanowi on “tylko” bibliotekę do tworzenia interfejsów użytkownika – Angular jako pełnoprawny framework zawiera sporo funkcjonalności, których w React’cie domyślnie nie ma.

Pomiary

Sposób pomiaru

Jak już wcześniej wspomnieliśmy, pomiary czasu/pamięci zostały wykonane za pomocą wbudowanego narzędzia deweloperskiego w Google Chrome (zakładka Performance). Mierzyliśmy ogólny czas wykonania danego zadania, bazując na widocznych wywołaniach funkcji we wspomnianym narzędziu. Na załączonym obrazku zaznaczony został obszar, w którym znajdują się funkcje powodujące render elementów. Zostały one wykonane od wydarzenia “mouseup” co w naszym przypadku oznaczało kliknięcie na opcję z listy oznaczającą ilość elementów. Kolejne kroki to przeliczanie układu strony i wewnętrzne (w tym przypadku reactowe) wywołania.

Sam pomiar wyglądał następująco:

  1. Odświeżamy stronę.
  2. Uruchamiamy nagrywanie profilu w narzędziu.
  3. Wykonujemy po kolei zadania: 
    1. wybieramy ilość elementów,
    2. dodajemy element,
    3. usuwamy element,
    4. zmieniamy wszystkie elementy.
  4. Kończymy nagrywanie profilu.
  5. Analizujemy wyniki poprzez zaznaczanie odpowiednich przedziałów. 

Warto też dodać, że ostateczne pomiary to średnia 10 wyników. W dodatku zostały wykonane na dwóch różnych maszynach. Żeby zachować rzetelność wyników, każdy pomiar został wykonany w trybie “incognito”. 

Przejdźmy zatem do testów. 

Pierwsze wyrenderowanie elementów

To dobry moment, aby wspomnieć o rozmiarach produkcyjnych paczek aplikacji – dla Angulara jest to 166,98 kB, natomiast dla Reacta 48,44 kB.

Powyższy wykres przedstawia zależność czasu od ilości renderowanych elementów. Przy małej liczbie (10 i 100) całkowity czas renderu, mimo że się różnił, w żaden sposób nie wpływał na komfort użytkowania aplikacji. Można jednakże zauważyć, że w przypadku 100 elementów różnica w zmierzonym czasie była dwukrotna. Problemy zaczynają się przy próbie wyświetlenia 1000 elementów jednocześnie. Tutaj React zdecydowanie wysuwa się na przód, gdyż średni czas jest ponad trzykrotnie mniejszy. Różnica była widoczna gołym okiem podczas samych testów. Wraz ze zwiększaniem się liczby elementów przepaść pomiędzy Reactem a Angularem stawała się coraz głębsza. Warto zwrócić też uwagę na to, iż w obu przypadkach różnica między 10 000 a 20 000 była mniej więcej dwukrotna. Możemy więc założyć, że zależność pomiędzy ilością elementów a czasem jest liniowa.

Przerenderowanie elementów

Na powyższym wykresie przedstawiono zależność czasu od ilości elementów przy ich przerenderowywaniu. Tutaj, analogicznie jak przy pierwszym renderowaniu, React wysuwa się zdecydowanie naprzód. Znacząca różnica jest widoczna przy liczbie 1000 elementów. W przypadku 20 000 elementów różnica jest aż pięciokrotna. Najprawdopodobniej wynika to z tego, iż React bardzo sprawnie radzi sobie ze zmienianiem komponentów poprzez zastosowanie Virtual DOM. Jak widać, w praktyce to podejście jest wydajniejsze od rozwiązania angularowego, przynajmniej jeśli chodzi o aspekt czasowy. 

Dodanie nowego elementu 

Kolejny wykres przedstawia zależność czasu od liczby elementów dla dodawania nowego elementu do listy. Tutaj sprawa wygląda już nieco inaczej. Angular prawie we wszystkich przypadkach (oprócz 10 elementów) wypadł zdecydowanie lepiej. Co ciekawe, największa różnica jest dla 1000 elementów, gdzie Angular był średnio ponad cztery razy szybszy. Zaobserwować też można pewne wypłaszczenie się wykresu między 10 000 a 20 000 elementami. Różnica bezwzględna w czasie pomiędzy Angularem i Reactem jest wtedy dość podobna. Z czego może wynikać przewaga Angulara w tym przypadku? Najprawdopodobniej przyczyną jest struktura aplikacji. W teście założyliśmy, że korzystamy z metody “props-drilling” do zarządzania stanem w React’cie. Jako że komponent do wyświetlania listy był w innym miejscu niż przycisk do jej odświeżania, praktycznie cała struktura aplikacji musiała zostać przerenderowana. Możliwe, że ten narzut jest spowodowany właśnie ponownym renderowaniem rzeczy, które niekoniecznie musiały się wyrenderować. 

Usunięcie elementu

Został jeszcze wykres zależności czasu od liczby elementów w przypadku usunięcia jednego z nich. Tutaj różnice nie są aż tak duże, jednak trzeba przyznać, że Angular znów jest na prowadzeniu. Przyczyny tego mogą być podobne jak przy dodawaniu elementów, tzn. “props-drilling” powoduje, że mogą się przerenderować węzły HTML-owe, które niekoniecznie brały udział w operacji usuwania. Mimo tego czas usuwania w przypadku wielu elementów można uznać za akceptowalny w obu przypadkach. 

Zużycie pamięci

Na sam koniec zostaje nam analiza zużycia pamięci we wszystkich wymienionych wyżej scenariuszach. Do przedstawienia danych wykorzystaliśmy wykres świecowy, gdzie każda świeca dostarcza informacji o średnim zużyciu pamięci oraz wartościach skrajnych. Ciało świecy przedstawia zakres pomiędzy średnim minimalnym a średnim maksymalnym zużyciem pamięci. Knoty świecy natomiast odpowiadają za skrajne wartości – jednorazowe minimum oraz maksimum wyciągnięte ze zbioru wszystkich pomiarów. Świeczki na wykresie są pogrupowane: każda para “rozmiar listy + akcja” ma dwie świeczki, z których niebieska (lewa) reprezentuje Angulara, a biała (prawa) – Reacta. 

Odpowiednie podpisy znajdują się na osi X, szersze odstępy oddzielają sekcje dotyczące danego rozmiaru listy. Przy 10 i 100 elementach zużycie pamięci jest generalnie bardzo niskie. Przy 10 delikatnie lepiej wypada React, a przy 100 – równie delikatnie lepiej – Angular. Przy 1000 elementów w szczytowych momentach zużycie sięga 20 MB i, co zaskakujące, za wynik ten odpowiada Angular. Przy takim rozmiarze listy React minimalnie przegrał tylko w momencie ponownego renderowania całej listy. Dla większych rozmiarów listy wyniki dla Reacta zaczynają dynamicznie rosnąć, w tym widoczny jest wzrost wraz z czasem działania aplikacji. Angular nie ma tego problemu, wzrost pamięci jest bardziej schodkowy. Każda kolejna akcja mieści się w zakresie pamięci potrzebnym do pierwszego renderowania. React ciągle rośnie w czasie działania aplikacji. To sygnał, że trzeba dobrze zadbać o optymalizację, jeżeli mamy w aplikacji sporą interaktywną listę.

Podsumowanie

Rozprawiliśmy się z porównaniem wydajności aplikacji napisanych z pomocą Angulara i Reacta. Jest to jednak tylko jeden z czynników, jakimi można się kierować, dobierając technologie do swojego projektu, co więcej, rzadko kiedy powinien być tym przeważającym. 

O pozostałych czynnikach porozmawiamy na webinarze 15. września, gdzie spróbujemy odpowiedzieć na pytanie: “Kiedy React jest śrubokrętem, a kiedy wkrętarką?”. Już teraz serdecznie na niego zapraszamy!

Jeśli nie możecie doczekać się webinaru to poniżej dostępny jest pierwszy odcinek naszej nowej serii “Synergy Caffe”, który również dotyczy porównania Angulara i Reacta. Zachęcamy do śledzenia całej serii, kolejny odcinek już niebawem!


Zdjęcie główne artykułu pochodzi z unsplash.com.

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
Nowy switch w C# 8.0. Jak działa Property Pattern