Przejdź do treści głównej

W stronę Hermesa jako domyślnego silnika

· 12 minut czytania
Xuan Huang
Xuan Huang
Software Engineer @ Meta
Nieoficjalne Tłumaczenie Beta

Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →

Od ogłoszenia Hermesa w 2019 roku, rozwiązanie to zdobywa coraz większą popularność w społeczności. Zespół Expo, twórców popularnego meta-frameworku dla aplikacji React Native, niedawno ogłosił eksperymentalne wsparcie dla Hermesa po tym, jak było to jedną z najbardziej wyczekiwanych funkcji w Expo. Zespół Realm, popularnej bazy danych mobilnych, również niedawno dostarczył wsparcie w wersji alfa dla Hermesa. W tym artykule chcemy przedstawić najważniejsze postępy, które poczyniliśmy w ciągu ostatnich dwóch lat, aby Hermes stał się najlepszym silnikiem JavaScript dla React Native. Patrząc w przyszłość, jesteśmy przekonani, że dzięki tym ulepszeniom i kolejnym, które nadejdą, możemy uczynić Hermesa domyślnym silnikiem JavaScript dla React Native na wszystkich platformach.

Optymalizacja pod kątem React Native

Kluczową cechą Hermesa jest wykonywanie pracy kompilacyjnej z wyprzedzeniem (AOT), co oznacza, że aplikacje React Native z włączonym Hermesem dostarczane są z prekompilowanym, zoptymalizowanym bajtkodem zamiast zwykłego kodu źródłowego JavaScript. To drastycznie zmniejsza ilość pracy potrzebnej do uruchomienia aplikacji dla użytkowników. Pomiarów z aplikacji Facebooka i społecznościowych wskazują, że włączenie Hermesa często skraca metrykę TTI (czyli czasu do interaktywności) produktu o niemal połowę.

Mimo to pracowaliśmy nad ulepszeniem Hermesa w wielu innych aspektach, aby stał się jeszcze lepszym silnikiem JavaScript specjalnie dostosowanym do React Native.

Budowa nowego garbage collectora dla Fabric

Dzięki nadchodzącemu rendererowi Fabric w nowej architekturze React Native, będzie możliwe synchroniczne wywoływanie JavaScriptu w wątku UI. Oznacza to jednak, że jeśli wątek JavaScriptu zajmie zbyt dużo czasu na wykonanie, może to spowodować zauważalne spadki klatek interfejsu i blokować dane wejściowe użytkownika. Renderowanie współbieżne umożliwione przez Fiber w React będzie unikać planowania długich zadań JavaScriptu poprzez dzielenie pracy renderowania na fragmenty. Istnieje jednak inne częste źródło opóźnień z wątku JavaScript — gdy silnik JavaScriptu musi "zatrzymać świat" (stop the world) aby wykonać garbage collection (GC).

Poprzedni domyślny garbage collector w Hermesie, GenGC, był jednowątkowym, generacyjnym odśmieniaczem. Nowa generacja używa typowej strategii kopiowania semi-space, a stara generacja wykorzystuje strategię mark-compact, co sprawia, że jest bardzo skuteczny w agresywnym zwracaniu pamięci do systemu operacyjnego. Ze względu na jednowątkowość, GenGC ma wadę powodowania długich pauz GC. W aplikacjach tak złożonych jak Facebook dla Androida zaobserwowaliśmy średnią pauzę 200 ms lub 1,4 s przy p99. Widzieliśmy nawet pauzy dochodzące do 7 sekund, biorąc pod uwagę ogromną i zróżnicowaną bazę użytkowników Facebooka dla Androida.

Aby temu zaradzić, zaimplementowaliśmy zupełnie nowy głównie współbieżny odśmiecacz (GC) o nazwie Hades. Hades zarządza młodą generacją dokładnie tak samo jak GenGC, ale do starej generacji stosuje strategię mark-sweep z migawką początkową, co znacząco redukuje czas pauzowania GC poprzez wykonywanie większości pracy w wątku w tle bez blokowania głównego wątku silnika wykonującego kod JavaScript. Nasze statystyki pokazują, że Hades pauzuje jedynie na 48ms przy p99.9 na urządzeniach 64-bitowych (34-krotnie szybciej niż GenGC!) i około 88ms przy p99.9 na urządzeniach 32-bitowych (gdzie działa jako inkrementalny GC w jednym wątku). Te poprawy czasu pauzowania mogą kosztować ogólną przepustowość z powodu droższych barier zapisu, wolniejszej alokacji opartej na freelist (w przeciwieństwie do alokatora bump pointer) oraz większej fragmentacji sterty. Uważamy, że to właściwe kompromisy, a dodatkowo udało nam się osiągnąć niższe zużycie pamięci dzięki scalaniu i innym optymalizacjom, o których opowiemy.

Skupienie na newralgicznych punktach wydajności

Czas uruchamiania aplikacji jest kluczowy dla sukcesu wielu produktów, dlatego nieustannie przesuwamy granice możliwości React Native. Dla każdej nowej funkcji JavaScript implementowanej w Hermesie dokładnie monitorujemy jej wpływ na wydajność w produkcji i zapewniamy brak regresji metryk. W Facebooku testujemy obecnie dedykowany profil transformacji Babel dla Hermesa w Metro, zastępujący kilkanaście transformacji Babel natywnymi implementacjami ESNext w Hermesie. Zaobserwowaliśmy 18-25% poprawy TTI na wielu ekranach oraz ogólny spadek rozmiaru bajtkodu i spodziewamy się podobnych rezultatów w środowisku open source.

Poza wydajnością startu zidentyfikowaliśmy zużycie pamięci jako obszar do poprawy w aplikacjach React Native, szczególnie w kontekście rzeczywistości wirtualnej. Dzięki niskopoziomowej kontroli jako silnika JavaScript, przeprowadziliśmy serie optymalizacji pamięciowych, dosłownie wyciskając każdy bajt:

  1. Wcześniej wszystkie wartości JavaScript były reprezentowane jako 64-bitowe wartości tagowane z kodowaniem NaN-boxing dla liczb zmiennoprzecinkowych i wskaźników na architekturze 64-bit. W praktyce jest to marnotrawstwo, ponieważ większość liczb to małe liczby całkowite (SMI), a sterta JavaScript aplikacji klienckich rzadko przekracza 4GiB. Wprowadziliśmy nowe 32-bitowe kodowanie, gdzie SMI i wskaźniki są kodowane w 29 bitach (wskaźniki są wyrównane do 8 bajtów, więc 3 najmłodsze bity są zawsze zerowe), a pozostałe liczby JS są boxowane na stercie. To zmniejszyło rozmiar sterty JavaScript o ~30%.

  2. Różne rodzaje obiektów JavaScript są reprezentowane jako różne komórki zarządzane przez GC na stercie. Dzięki agresywnej optymalizacji układu pamięci ich nagłówków zmniejszyliśmy zużycie pamięci o kolejne ~15%.

Kluczową decyzją w Hermesie była rezygnacja z implementacji kompilatora just-in-time (JIT), ponieważ uważamy, że dla większości aplikacji React Native dodatkowe koszty rozgrzewania i zwiększone zużycie pamięci/binarek nie byłyby opłacalne. Przez lata inwestowaliśmy w optymalizację wydajności interpretera i optymalizacje kompilatora, aby dorównać konkurencyjnym silnikom w obciążeniach React Native. Nadal skupiamy się na poprawie przepustowości poprzez identyfikację wąskich gardeł (pętla dispatch interpretera, układ stosu, model obiektowy, GC itd.). Spodziewajcie się kolejnych liczb w nadchodzących wydaniach!

Pionierskie integracje pionowe

W Facebooku preferujemy współlokowanie projektów w dużym monorepo. Dzięki ścisłej współpracy silnika (Hermes) i hosta (React Native) otworzyliśmy przestrzeń dla integracji pionowych. Przykłady:

  • Hermes obsługuje debugowanie JavaScript bezpośrednio na urządzeniu za pomocą debugera Chrome poprzez implementację protokołu Chrome DevTools. To rozwiązanie przewyższa starsze "Zdalne debugowanie JS" (które używa proxy do uruchamiania kodu w przeglądarce Chrome na komputerze), ponieważ umożliwia debugowanie synchronicznych wywołań natywnych i gwarantuje spójne środowisko wykonawcze. Wraz z React DevTools, Metro, Inspektorem i innymi narzędziami, debuger Hermesa stał się częścią Flippera, oferując kompleksowe środowisko dla programistów.

  • Obiekty alokowane podczas inicjalizacji aplikacji React Native często są długożyjące i nie spełniają hipotezy generacyjnej wykorzystywanej przez odśmiewacze generacyjne. Dlatego skonfigurowaliśmy Hermesa w React Native do alokacji pierwszych 32 MiB bezpośrednio w starszych generacjach (tzw. pre-tenuring), co zapobiega wyzwalaniu pauz odśmiecania i opóźnieniom TTI.

  • Nowa architektura React Native w dużej mierze opiera się na JSI (JavaScript Interface) – lekkim, uniwersalnym API do osadzania silnika JavaScript w aplikacjach C++. Dzięki temu, że zespół odpowiedzialny za silnik JS utrzymuje również implementację JSI, możemy zapewnić optymalną integrację przetestowaną w skali Facebooka: niezawodną, wydajną i gotową na ekstremalne obciążenia.

  • Poprawne semantycznie i wydajne implementacje mechanizmów współbieżności w JavaScript (np. promisy) i na poziomie platformy (np. mikrozadania) są kluczowe dla współbieżnego renderowania w Reakcie i przyszłości aplikacji React Native. Historycznie promisy w React Native były polyfillowane przy użyciu niestandardowego API setImmediate. Obecnie pracujemy nad udostępnieniem natywnych promisów i mikrozadań poprzez JSI oraz wprowadzeniem queueMicrotask – nowego standardu sieciowego – dla lepszego wsparcia współczesnego asynchronicznego kodu JavaScript.

Zaangażowanie całej społeczności

Hermes sprawdził się doskonale w Facebooku, ale nasza misja zakończy się dopiero wtedy, gdy społeczność będzie mogła wykorzystywać go do budowania doświadczeń w całym ekosystemie, aby każdy mógł korzystać z pełni jego możliwości.

Ekspansja na nowe platformy

Hermes został początkowo udostępniony jako open source tylko dla React Native na Androida. Z radością obserwowaliśmy, jak społeczność rozszerza jego wsparcie na wielu innych platformach obsługiwanych przez ekosystem React Native.

Callstack przewodził pracom nad przeniesieniem Hermesa na iOS w React Native 0.64. Zespół opublikował serię artykułów i podcast opisujących tę implementację. Według ich testów, Hermes zapewnił ~40% poprawy czasu uruchamiania i ~18% redukcji zużycia pamięci na iOS w aplikacji Mattermost w porównaniu z JSC, przy jedynie 2.4 MiB narzutu na rozmiar aplikacji. Zachęcam do zobaczenia wyników na własne oczy.

Microsoft pracuje nad wsparciem Hermesa w React Native dla Windows i macOS. Podczas Microsoft Build 2020 firma ujawniła, że wpływ Hermesa na pamięć (zbiór roboczy) jest o 13% niższy niż silnika Chakra w React Native dla Windows. Ostatnie syntetyczne testy wykazały, że Hermes 0.8 (z Hadesem i wspomnianymi optymalizacjami SMI oraz kompresją wskaźników) zużywa o 30%-40% mniej pamięci niż inne silniki. Nic dziwnego, że Messenger na komputery z funkcją wideorozmów, zbudowany na React Native, również korzysta z Hermesa.

Wreszcie, Hermes napędza także wszystkie doświadczenia wirtualnej rzeczywistości budowane z wykorzystaniem technologii React na platformie Oculus, w tym Oculus Home.

Wsparcie dla społeczności

Zdajemy sobie sprawę, że wciąż istnieją przeszkody uniemożliwiające części społeczności przyjęcie Hermesa. Zobowiązujemy się do rozwijania brakujących funkcji, aby Hermes stał się kompletnym rozwiązaniem odpowiednim dla większości aplikacji React Native. Oto jak społeczność już wpłynęła na plan rozwoju Hermesa:

Podsumowanie

Podsumowując, naszą wizją jest przygotowanie Hermesa do roli domyślnego silnika JavaScript na wszystkich platformach React Native. Już rozpoczęliśmy prace w tym kierunku i chcemy poznać Wasze opinie na ten temat.

Niezwykle ważne jest przygotowanie ekosystemu do płynnego wdrożenia. Zachęcamy do testowania Hermesa i zgłaszania uwag, pytań, próśb o funkcje oraz problemów z kompatybilnością w naszym repozytorium GitHub.

Podziękowania

Serdecznie dziękujemy zespołowi Hermes, zespołowi React Native oraz licznym współtwórcom ze społeczności React Native za ich wkład w rozwój Hermesa.

Osobiste podziękowania kieruję (w kolejności alfabetycznej) do: Eli White, Luny Wei, Neila Dhar, Tima Yunga, Tzvetana Mikova oraz wielu innych za pomoc przy tworzeniu tego artykułu.