Optymalizacja uruchamiania JavaScript podczas uruchamiania

Ponieważ strony, które tworzymy, są coraz bardziej zależne od JavaScriptu, czasami płacimy za to, co wysyłamy, w sposób, który nie zawsze jest łatwy do zauważenia. Z tego artykułu dowiesz się, dlaczego odrobina dyscypliny może Ci pomóc, jeśli chcesz, aby Twoja witryna wczytywała się i działała szybko na urządzeniach mobilnych. Przesyłanie mniejszej ilości kodu JavaScriptu może oznaczać krótszy czas przesyłania przez sieć, mniejszy czas potrzebny na rozpakowanie kodu oraz krótszy czas potrzebny na analizowanie i kompilowanie tego kodu.

Sieć

Gdy większość deweloperów myśli o kosztach JavaScriptu, ma na myśli koszt pobierania i wykonywania. Wysyłanie większej liczby bajtów kodu JavaScriptu przez sieć zajmuje więcej czasu, gdy połączenie użytkownika jest wolniejsze.

Gdy przeglądarka poprosi o zasób, musi on zostać pobrany i rozpakowany. W przypadku zasobów takich jak JavaScript muszą one zostać przeanalizowane i skompilowane przed wykonaniem.

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

Możesz obniżyć koszty przesyłania JavaScriptu w sieci, wykonując te czynności:

  • Wysyłanie tylko kodu potrzebnego użytkownikowi.
    • Użyj dzielenia kodu, aby podzielić kod JavaScript na część krytyczne i niekrytyczne. Pakiety modułów, takie jak webpack, obsługują dzielenie kodu.
    • Leniwe ładowanie kodu, który nie jest krytyczny.
  • Minifikacja
  • Kompresja
    • Do kompresji zasobów tekstowych należy używać co najmniej gzip.
    • Rozważ użycie Brotli.~q11. Brotli jest lepszy od gzip pod względem współczynnika kompresji. Dzięki temu CertSimple udało się zaoszczędzić 17% na rozmiarze skompresowanych bajtów kodu JS, a LinkedIn zaoszczędził 4% na czasie wczytywania.
  • Usuwanie nieużywanego kodu.
  • Korzystanie z pamięci podręcznej kodu w celu zminimalizowania połączeń z internetem.
    • Użyj pamięci podręcznej HTTP, aby zapewnić skuteczne przechowywanie odpowiedzi w pamięci podręcznej przeglądarki. Określ optymalny czas życia skryptów (max-age) i dostarczaj tokeny walidacyjne (ETag), aby uniknąć przesyłania niezmienionych bajtów.
    • Buforowanie w usłudze workera może zwiększyć odporność sieci aplikacji i zapewnić Ci szybki dostęp do funkcji takich jak pamięć podręczna kodu V8.
    • Używaj długoterminowego buforowania, aby uniknąć konieczności ponownego pobierania zasobów, które się nie zmieniły. Jeśli używasz Webpacka, zapoznaj się z artykułem o hashowaniu nazw plików.

Analizowanie/kompilowanie

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

ALT_TEXT_HERE

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

ALT_TEXT_HERE
Narzędzie DevTools w Chrome – panel Wydajność > Od dołu Po włączeniu funkcji Runtime Call Stats w V8 możemy zobaczyć czas spędzony na poszczególnych etapach, takich jak parsowanie i kompilowanie

Ale dlaczego to ma znaczenie?

ALT_TEXT_HERE

Długi czas analizowania i kompilowania kodu może znacznie opóźnić interakcję użytkownika z Twoją witryną. Im więcej kodu JavaScriptu przesyłasz, tym dłużej trwa jego analizowanie i skompilowanie, zanim witryna stanie się interaktywna.

„Byte-for-byte, JavaScript is more expensive for the browser to process than the equivalently sized image or Web Font” – Tom Dale

W porównaniu z JavaScriptem przetwarzanie obrazów o równych rozmiarach wiąże się z licznymi kosztami (trzeba je jeszcze zdekodować), ale na przeciętnym sprzęcie mobilnym JavaScript może negatywnie wpływać na interaktywność strony.

ALT_TEXT_HERE
Koszt bajtów kodu JavaScript i obrazu jest bardzo różny. Obrazy zwykle nie blokują głównego wątku ani nie uniemożliwiają interfejsom korzystania z interakcji podczas dekodowania i rasteryzacji. Jednak kod JS może opóźniać interaktywność ze względu na koszty analizowania, kompilowania i wykonywania.

Gdy mówimy o powolnym parsowaniu i kompilowaniu, kontekst jest ważny – mówimy tu o przeciętnych telefonach komórkowych. Użytkownicy korzystający z telefonów z wolnymi procesorami i kartami graficznymi, bez pamięci podręcznej L2/L3, a czasem nawet z ograniczoną ilością pamięci.

Możliwości sieci i urządzeń nie zawsze są zgodne. Użytkownik z niesamowitym połączeniem światłowodowym niekoniecznie ma najlepszy procesor do analizowania i interpretowania kodu JavaScript wysyłanego na jego urządzenie. To samo dotyczy odwrotnej sytuacji: kiepskie połączenie z internetem, ale błyskawicznie szybki procesor. – Kristofer Baxter, LinkedIn

Poniżej możesz zobaczyć koszt parsowania około 1 MB skompresowanego (prostego) kodu JavaScript na sprzęcie niskiego i wysokiego poziomu. Czas analizowania i kompilowania kodu jest 2–5 razy dłuższy na najszybszych telefonach na rynku niż na przeciętnych telefonach.

ALT_TEXT_HERE
Ten wykres przedstawia czasy analizowania pakietu kodu JavaScript o rozmaju 1 MB (skompresowanego do ~250 KB) na różnych komputerach i urządzeniach mobilnych. Przy obliczaniu kosztu parsowania należy wziąć pod uwagę rozmiar po rozpakowaniu, np.skompresowany kod JS o rozmiarze około 250 KB zajmuje po rozpakowaniu około 1 MB.

A co z witryną w świecie rzeczywistym, np. CNN.com?

Na zaawansowanym telefonie iPhone 8 parsowanie i skompilowanie kodu JS z serwisu CNN zajmuje tylko ok. 4 s, podczas gdy na przeciętnym telefonie (Moto G4) trwa to ok. 13 s. Może to mieć znaczący wpływ na szybkość, z jaką użytkownik może w pełni korzystać z tej witryny.

ALT_TEXT_HERE
Powyżej widzisz czasy analizowania porównujące wydajność układu Apple A11 Bionic z Snapdragonem 617 na przeciętnym sprzęcie z Androidem.

Dlatego ważne jest, aby testować na przeciętnym sprzęcie (np. Moto G4), a nie tylko na telefonie, który masz w kieszeni. Kontekst ma znaczenie: optymalizuj pod kątem urządzenia i warunków sieci, z których korzystają użytkownicy.

ALT_TEXT_HERE
Google Analytics może dostarczyć Ci informacji o klasach urządzeń mobilnych, z których użytkownicy otwierają Twoją witrynę. Dzięki temu możesz dowiedzieć się, jakie są rzeczywiste ograniczenia procesora i procesora graficznego.

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

Korzystając z archiwum HTTP (około 500 najpopularniejszych witryn), przeanalizowaliśmy stan JavaScriptu na urządzeniach mobilnych. Okazało się, że 50% witryn potrzebuje ponad 14 sekund na wczytanie. Te witryny poświęcają na analizowanie i kompilowanie kodu JS nawet 4 sekundy.

ALT_TEXT_HERE

Dodaj do tego czas potrzebny na pobieranie i przetwarzanie kodu JavaScript i innych zasobów, a nie będzie Cię dziwić, że użytkownicy muszą czekać, zanim będą mogli korzystać z witryn. Z pewnością możemy to zrobić lepiej.

Usunięcie z stron niekrytycznych skryptów JavaScript może skrócić czas transmisji, analizowania i kompilowania oraz potencjalne obciążenie pamięci. Pomaga to też szybciej wczytywać interaktywne elementy strony.

Czas wykonywania

Koszt może się wiązać nie tylko z analizą i skompilowaniem. Wykonanie kodu JavaScript (uruchamianie kodu po przeanalizowaniu i skompilowaniu) to jedna z operacji, które muszą być wykonywane w wątku głównym. Długi czas wykonywania może też opóźniać czas, po którym użytkownik może wejść w interakcję z Twoją witryną.

ALT_TEXT_HERE

Jeśli skrypt jest wykonywany przez ponad 50 ms, czas do interakcji wydłuża się o cały czas potrzebny na pobranie, skompilowanie i wykonanie kodu JS.

Aby tego uniknąć, kod JavaScriptu powinien być podzielony na małe fragmenty, aby nie blokować wątku głównego. Sprawdź, czy możesz zmniejszyć ilość pracy wykonywanej podczas wykonywania kodu.

Inne koszty

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

  • Pamięć. Ze względu na GC (garbage collection) strony mogą wydawać się niepłynne lub często się zatrzymywać. Gdy przeglądarka odzyskuje pamięć, wykonywanie kodu JS jest wstrzymywane, więc przeglądarka często zbierająca śmieci może wstrzymywać wykonywanie kodu częściej, niż byśmy chcieli. Aby uniknąć wycieku pamięci i częstych przerw w działaniu GC, zadbaj o płynne działanie stron.
  • Podczas działania długotrwały kod JavaScript może blokować wątek główny, powodując, że strony przestają odpowiadać. 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 czasu od interakcji do kolejnego wyrenderowania (INP).

Sposoby zmniejszania kosztów dostarczania kodu JavaScript

Jeśli chcesz, aby czasy analizowania/kompilowania i przesyłania sieciowego kodu JavaScript były jak najkrótsze, możesz skorzystać z takich wzorów jak podział na fragmenty na podstawie trasy lub PRPL.

PRPL

PRPL (Push, Render, Pre-cache, Lazy-load) to wzór, który optymalizuje interaktywność dzięki agresywnemu dzieleniu kodu i używaniu pamięci podręcznej:

ALT_TEXT_HERE

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

Korzystając z funkcji Runtime Call Stats w V8, analizujemy czas wczytywania popularnych witryn mobilnych i progresywnych aplikacji internetowych. Jak widać, czas analizowania (podany na pomarańczowo) zajmuje znaczną część czasu, jaki użytkownicy spędzają na tych stronach:

ALT_TEXT_HERE

Wego to witryna, która korzysta z PRPL. Dzięki temu udało się jej utrzymać krótki czas analizowania tras, dzięki czemu użytkownicy mogą bardzo szybko uzyskać interaktywność. Wiele z tych witryn wdrożyło podział kodu i budżety na wydajność, aby obniżyć koszty kodu JS.

Bootstrapping progresywny

Wiele witryn optymalizuje widoczność treści kosztem interakcji. Aby uzyskać szybkie wyświetlenie pierwszego pokolorowania, gdy masz duże pakiety JavaScriptu, deweloperzy czasami stosują renderowanie po stronie serwera, a potem „ulepszają” je, aby dołączyć przetwarzanie zdarzeń, gdy kod JavaScriptu zostanie w końcu pobrany.

Uważaj – wiąże się to z dodatkowymi kosztami. 1) zwykle wysyłasz większy kod HTML, który może zwiększać interaktywność, 2) możesz pozostawić użytkownika w niezwykłej dolinie, w której połowa interakcji nie może być interaktywna, dopóki JavaScript nie zakończy przetwarzania.

Lepszym rozwiązaniem może być uruchamianie aplikacji stopniowo. Przesyłanie strony o minimalnej funkcjonalności (składającej się tylko z kodu HTML/JS/CSS potrzebnego do bieżącej ścieżki). Gdy do aplikacji docierają kolejne zasoby, może ona ładować je z opóźnieniem i odblokowywać kolejne funkcje.

ALT_TEXT_HERE
Bootstrapping, Paul Lewis

Ładowanie kodu proporcjonalnie do tego, co jest widoczne, to święty Graal. W tym celu możesz wykorzystać metody PRPL i progresywnego uruchamiania.

Podsumowanie

Rozmiar transmisji ma kluczowe znaczenie w przypadku sieci niskiego poziomu. Czas analizowania jest ważny w przypadku urządzeń z procesorem ograniczonym. Utrzymywanie ich na niskim poziomie jest ważne.

Zespół odniósł sukces dzięki zastosowaniu ścisłych budżetów skuteczności, które pozwoliły skrócić czas przesyłania i analizowania/kompilowania kodu JavaScript. Zobacz film „Can You Afford It?: „Real-world Web Performance Budgets” (Budżety wydajności witryn w rzeczywistych warunkach)

ALT_TEXT_HERE
Warto zastanowić się, ile „zapasu” w JS daje nam przyjęta architektura pod kątem logiki aplikacji.

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

Więcej informacji