Optymalizacja uruchamiania JavaScriptu

Gdy tworzymy witryny, które w dużym stopniu opierają się na języku JavaScript, czasami płacimy za to, co wysyłamy, w sposób, który nie zawsze jest łatwy do zauważenia. W tym artykule wyjaśnimy, dlaczego odrobina dyscypliny może pomóc, jeśli chcesz, aby Twoja witryna szybko się wczytywała i była interaktywna na urządzeniach mobilnych. Dostarczanie mniejszej ilości kodu JavaScript może oznaczać krótszy czas transmisji sieciowej, mniej czasu poświęcanego na dekompresję kodu oraz krótszy czas analizowania i kompilowania tego kodu.

Sieć

Gdy większość deweloperów myśli o koszcie JavaScriptu, myśli o koszcie pobierania i wykonywania. Przesyłanie większej liczby bajtów kodu JavaScript przez sieć trwa dłużej, im wolniejsze jest połączenie użytkownika.

Gdy przeglądarka żąda zasobu, musi on zostać pobrany, a następnie zdekompresowany. W przypadku zasobów takich jak JavaScript muszą one zostać przeanalizowane i skompilowane przed wykonaniem.

Może to być problem, ponieważ efektywny typ połączenia sieciowego użytkownika może nie być połączeniem 3G, 4G ani Wi-Fi. Możesz korzystać z Wi-Fi w kawiarni, ale mieć połączenie z hotspotem komórkowym o szybkości 2G.

Możesz obniżyć koszt przesyłania kodu JavaScript przez sieć, wykonując te czynności:

  • Wysyłanie tylko kodu, którego potrzebuje użytkownik.
    • Użyj dzielenia kodu, aby podzielić kod JavaScript na krytyczny i niekrytyczny. Narzędzia do tworzenia pakietów modułów, takie jak webpack, obsługują dzielenie kodu.
    • Leniwe ładowanie kodu, który nie jest kluczowy.
  • Minifikacja
  • Kompresja
    • Do kompresowania zasobów tekstowych używaj co najmniej gzip.
    • Rozważ użycie Brotli ~q11. Brotli przewyższa gzip pod względem współczynnika kompresji. Pomogło to firmie CertSimple zaoszczędzić 17% na rozmiarze skompresowanych bajtów JS, a LinkedInowi – 4% na czasie wczytywania.
  • Usuwanie nieużywanego kodu.
  • Zapisywanie kodu w pamięci podręcznej w celu zminimalizowania liczby żądań sieciowych.
    • Używaj buforowania HTTP, aby przeglądarki skutecznie zapisywały odpowiedzi w pamięci podręcznej. Określ optymalny czas życia skryptów (max-age) i podaj tokeny weryfikacyjne (ETag), aby uniknąć przesyłania niezmienionych bajtów.
    • Pamięć podręczna skryptu service worker może zwiększyć odporność aplikacji na problemy z siecią i zapewnić szybki dostęp do funkcji takich jak pamięć podręczna kodu V8.
    • Używaj długiego buforowania, aby uniknąć ponownego pobierania zasobów, które nie uległy zmianie. Jeśli używasz Webpack, zapoznaj się z informacjami o haszowaniu nazw plików.

Analiza/kompilacja

Po pobraniu jednym z największych kosztów JavaScriptu jest czas, jaki silnik JS potrzebuje na przeanalizowanie/skompilowanie tego kodu. W Narzędziach deweloperskich w Chrome analizowanie i kompilowanie są częścią żółtego czasu „Skryptowanie” w panelu Wydajność.

ALT_TEXT_HERE

Karty Od dołu i Drzewo wywołań pokazują dokładne czasy analizowania i kompilowania:

ALT_TEXT_HERE
Panel Wydajność w Narzędziach deweloperskich w Chrome > Od dołu. Po włączeniu statystyk wywołań środowiska wykonawczego V8 możemy zobaczyć czas spędzony na etapach takich jak analiza i kompilacja.

Dlaczego to takie ważne?

ALT_TEXT_HERE

Długi czas analizowania i kompilowania kodu może znacznie opóźnić moment, w którym użytkownik może wejść w interakcję z witryną. Im więcej kodu JavaScript wyślesz, tym dłużej będzie trwało jego analizowanie i kompilowanie, zanim witryna stanie się interaktywna.

Bajt po bajcie, przetwarzanie kodu JavaScript jest dla przeglądarki bardziej kosztowne niż obrazu lub czcionki internetowej o takim samym rozmiarze – Tom Dale

W porównaniu z JavaScriptem przetwarzanie obrazów o podobnym rozmiarze wiąże się z wieloma kosztami (nadal trzeba je zdekodować), ale na przeciętnym sprzęcie mobilnym JS częściej negatywnie wpływa na interaktywność strony.

ALT_TEXT_HERE
Bajty JavaScriptu i obrazów mają bardzo różne koszty. Obrazy zwykle nie blokują głównego wątku ani nie uniemożliwiają interaktywności interfejsów podczas dekodowania i rasteryzacji. JS może jednak opóźniać interaktywność ze względu na koszty analizowania, kompilowania i wykonywania.

Gdy mówimy o powolnym analizowaniu i kompilowaniu, ważny jest kontekst – mamy na myśli przeciętne telefony komórkowe. Przeciętni użytkownicy mogą mieć telefony z wolnymi procesorami i GPU, bez pamięci podręcznej L2/L3, a nawet z ograniczoną pamięcią.

Możliwości sieci i urządzenia nie zawsze są takie same. Użytkownik z doskonałym połączeniem światłowodowym niekoniecznie musi mieć najlepszy procesor do analizowania i oceniania kodu JavaScript wysyłanego na jego urządzenie. To samo dotyczy sytuacji odwrotnej: fatalne połączenie sieciowe, ale błyskawiczny procesor. – Kristofer Baxter, LinkedIn

Poniżej widać koszt analizy ok. 1 MB zdekompresowanego (prostego) kodu JavaScript na sprzęcie niższej i wyższej klasy. Czas potrzebny na przeanalizowanie i skompilowanie kodu na najszybszych telefonach na rynku jest od 2 do 5 razy krótszy niż na przeciętnych telefonach.

ALT_TEXT_HERE
Ten wykres przedstawia czas analizowania pakietu JavaScript o rozmiarze 1 MB (ok. 250 KB po skompresowaniu gzip) na komputerach i urządzeniach mobilnych różnych klas. Podczas analizowania kosztu analizy należy wziąć pod uwagę dane po dekompresji, np. skompresowany kod JS o rozmiarze ok. 250 KB po dekompresji ma rozmiar ok. 1 MB.

A co z prawdziwą witryną, np. CNN.com?

Na wysokiej klasy iPhonie 8 analiza i kompilacja kodu JS CNN zajmuje tylko ok. 4 sekund, podczas gdy na przeciętnym telefonie (Moto G4) trwa to ok. 13 sekund. Może to znacząco wpłynąć na to, jak szybko użytkownik może w pełni korzystać z witryny.

ALT_TEXT_HERE
Powyżej widzimy porównanie czasów analizy wydajności układu Apple A11 Bionic z procesorem Snapdragon 617 w bardziej przeciętnym sprzęcie z Androidem.

To pokazuje, jak ważne jest testowanie na przeciętnym sprzęcie (np. Moto G4), a nie tylko na telefonie, który masz w kieszeni. Kontekst ma jednak znaczenie: optymalizuj pod kątem urządzeń i warunków sieciowych użytkowników.

ALT_TEXT_HERE
Google Analytics może dostarczyć informacji o klasach urządzeń mobilnych, z których korzystają użytkownicy Twojej witryny. Może to pomóc w zrozumieniu rzeczywistych ograniczeń procesora i GPU, z którymi borykają się użytkownicy.

Czy naprawdę wysyłamy za dużo kodu JavaScript? Err, possibly :)

Korzystając z archiwum HTTP (ok. 500 tys. najpopularniejszych witryn) do analizy stanu JavaScriptu na urządzeniach mobilnych, możemy zauważyć, że w przypadku 50% witryn uzyskanie interaktywności zajmuje ponad 14 sekund. Te witryny poświęcają nawet 4 sekundy na samo analizowanie i kompilowanie kodu JS.

ALT_TEXT_HERE

Jeśli weźmiemy pod uwagę czas potrzebny na pobranie i przetworzenie kodu JS oraz innych zasobów, nie powinno dziwić, że użytkownicy muszą czasem poczekać, zanim strony będą gotowe do użycia. Zdecydowanie możemy to poprawić.

Usunięcie ze stron niekrytycznego kodu JavaScript może skrócić czas transmisji, zmniejszyć obciążenie procesora podczas analizowania i kompilowania oraz ograniczyć potencjalne wykorzystanie pamięci. Dzięki temu strony szybciej stają się interaktywne.

Czas wykonywania

Kosztowne mogą być nie tylko analizowanie i kompilowanie. Wykonywanie kodu JavaScript (uruchamianie kodu po jego przeanalizowaniu/skompilowaniu) to jedna z operacji, które muszą być wykonywane w głównym wątku. Długi czas wykonywania może też opóźnić moment, w którym użytkownik może wejść w interakcję z witryną.

ALT_TEXT_HERE

Jeśli skrypt wykonuje się dłużej niż 50 ms, czas do pełnej interaktywności jest opóźniony o cały czas potrzebny na pobranie, skompilowanie i wykonanie kodu JS – Alex Russell

Aby temu zapobiec, warto podzielić kod JavaScript na małe fragmenty, co pozwoli uniknąć blokowania wątku głównego. Sprawdź, czy możesz zmniejszyć ilość pracy wykonywanej podczas realizacji.

Inne koszty

JavaScript może wpływać na wydajność strony na inne sposoby:

  • Pamięć. Strony mogą często zacinać się lub zatrzymywać z powodu GC (garbage collection). Gdy przeglądarka odzyskuje pamięć, wykonywanie kodu JS jest wstrzymywane, więc przeglądarka, która często zbiera niepotrzebne dane, może wstrzymywać wykonywanie kodu częściej, niż byśmy chcieli. Unikaj wycieków pamięci i częstych przerw na odśmiecanie pamięci, aby strony działały płynnie.
  • Podczas działania długo działający kod JavaScript może blokować główny wątek, co powoduje, że strony nie reagują. Dzielenie pracy na mniejsze części (za pomocą requestAnimationFrame() lub requestIdleCallback() do planowania) może zminimalizować problemy z responsywnością, co może pomóc w poprawie interakcji do kolejnego wyrenderowania (INP).

Wzorce zmniejszania kosztów dostarczania kodu JavaScript

Jeśli chcesz, aby czas analizowania/kompilowania i transmisji sieciowej kodu JavaScript był krótki, możesz skorzystać z takich wzorców jak dzielenie na fragmenty na podstawie trasy lub PRPL.

PRPL

PRPL (Push, Render, wstępne wczytywanie, Lazy-load) to wzorzec, który optymalizuje interaktywność dzięki agresywnemu dzieleniu kodu i buforowaniu:

ALT_TEXT_HERE

Zobaczmy, jaki może mieć wpływ.

Analizujemy czas wczytywania popularnych witryn mobilnych i progresywnych aplikacji internetowych, korzystając ze statystyk wywołań środowiska wykonawczego V8. Jak widać, czas analizy (zaznaczony na pomarańczowo) stanowi znaczną część czasu, który wiele z tych witryn poświęca na:

ALT_TEXT_HERE

Wego, witryna korzystająca z PRPL, utrzymuje krótki czas analizowania tras, dzięki czemu bardzo szybko staje się interaktywna. Wiele innych witryn wymienionych powyżej zastosowało podział kodu i budżety wydajności, aby obniżyć koszty JS.

Stopniowe uruchamianie

Wiele witryn optymalizuje widoczność treści kosztem interaktywności. Aby uzyskać szybkie pierwsze wyrenderowanie, gdy masz duże pakiety JavaScriptu, programiści czasami stosują renderowanie po stronie serwera, a następnie „ulepszają” je, aby dołączyć moduły obsługi zdarzeń, gdy JavaScript zostanie ostatecznie pobrany.

Zachowaj ostrożność – wiąże się to z kosztami. Zazwyczaj 1) wysyłasz większą odpowiedź HTML, która może zwiększyć interaktywność, 2) możesz pozostawić użytkownika w stanie, w którym połowa interfejsu nie może być interaktywna, dopóki JavaScript nie zakończy przetwarzania.

Lepszym rozwiązaniem może być progresywne uruchamianie. Wysyłaj stronę o minimalnej funkcjonalności (składającą się tylko z kodu HTML/JS/CSS potrzebnego do obsługi bieżącej trasy). W miarę pojawiania się kolejnych zasobów aplikacja może leniwie ładować i odblokowywać więcej funkcji.

ALT_TEXT_HERE
Progressive Bootstrapping, Paul Lewis

Wczytywanie kodu proporcjonalnie do tego, co jest widoczne, to święty Graal. Wzorce PRPL i progresywnego uruchamiania mogą pomóc w osiągnięciu tego celu.

Podsumowanie

Rozmiar transmisji ma kluczowe znaczenie w przypadku sieci o niskiej przepustowości. Czas analizy jest ważny w przypadku urządzeń z ograniczeniami procesora. Utrzymanie ich na niskim poziomie ma znaczenie.

Zespoły odniosły sukces, wprowadzając rygorystyczne budżety wydajności, aby utrzymać niskie czasy przesyłania i analizowania/kompilowania kodu JavaScript. Zobacz artykuł Alexa Russella „Can You Afford It?: Real-world Web Performance Budgets" (Budżety wydajności stron internetowych w rzeczywistych warunkach), aby dowiedzieć się więcej o budżetach na urządzenia mobilne.

ALT_TEXT_HERE
Warto zastanowić się, ile „miejsca” na JS pozostawiają nam decyzje architektoniczne, które podejmujemy, na potrzeby logiki aplikacji.

Jeśli tworzysz witrynę przeznaczoną na urządzenia mobilne, postaraj się, aby działała na reprezentatywnym sprzęcie, utrzymuj krótki czas analizowania i kompilowania kodu JavaScript oraz przyjmij budżet wydajności, aby Twój zespół mógł kontrolować koszty JavaScriptu.

Więcej informacji