Wczytywanie dużych zasobów JavaScriptu ma znaczący wpływ na szybkość strony. Podzielenie kodu JavaScript na mniejsze fragmenty i pobieranie tylko tego, co jest niezbędne do działania strony podczas uruchamiania, może znacznie poprawić czas reakcji podczas wczytywania, co z kolei może wpłynąć na interakcję z kolejnym wyrenderowaniem (INP).
Podczas pobierania, analizowania i kompilowania dużych plików JavaScript strona może przez pewien czas nie odpowiadać. Elementy strony są widoczne, ponieważ stanowią część początkowego kodu HTML strony i są stylizowane za pomocą CSS. Jednak JavaScript wymagany do obsługi tych elementów interaktywnych, a także inne skrypty wczytywane przez stronę mogą być analizowane i wykonywane, aby działały. W rezultacie użytkownik może odnieść wrażenie, że interakcja jest znacznie opóźniona lub nawet całkowicie przerwana.
Dzieje się tak często, ponieważ wątek główny jest blokowany, a JavaScript jest analizowany i kompilowany w wątku głównym. Jeśli ten proces trwa zbyt długo, interaktywne elementy strony mogą nie reagować wystarczająco szybko na działania użytkownika. Jednym ze sposobów na rozwiązanie tego problemu jest wczytywanie tylko tego kodu JavaScript, który jest potrzebny do działania strony, a pozostałego kodu JavaScript – później, za pomocą techniki zwanej dzieleniem kodu. W tej części skupimy się na drugiej z tych metod.
Skracanie czasu analizowania i wykonywania JavaScriptu podczas uruchamiania przez dzielenie kodu
Lighthouse wyświetla ostrzeżenie, gdy wykonanie JavaScriptu trwa dłużej niż 2 sekundy, a gdy trwa dłużej niż 3,5 sekundy, test kończy się niepowodzeniem. Nadmierne parsowanie i wykonywanie kodu JavaScript może stanowić problem w dowolnym momencie cyklu życia strony, ponieważ może zwiększyć opóźnienie przy pierwszym działaniu, jeśli czas interakcji użytkownika ze stroną zbiega się z momentem, w którym działają zadania głównego wątku odpowiedzialne za przetwarzanie i wykonywanie kodu JavaScript.
Co więcej, nadmierne wykonywanie i parsowanie kodu JavaScript jest szczególnie problematyczne podczas początkowego wczytywania strony, ponieważ to właśnie wtedy użytkownicy najprawdopodobniej będą wchodzić w interakcję ze stroną. W rzeczywistości całkowity czas blokowania (TBT), czyli wskaźnik responsywności wczytywania, jest silnie skorelowany z INP, co sugeruje, że użytkownicy często próbują wchodzić w interakcje ze stroną podczas jej wstępnego wczytywania.
Przydatny jest audyt Lighthouse, który podaje czas wykonywania każdego pliku JavaScript, o który prosi strona. Pomaga on dokładnie określić, które skrypty mogą być kandydatami do podziału kodu. Możesz też użyć narzędzia do sprawdzania pokrycia w Narzędziach deweloperskich Chrome, aby dokładnie określić, które części kodu JavaScript na stronie nie są używane podczas jej wczytywania.
Dzielenie kodu to przydatna technika, która może zmniejszyć początkowe ładunki JavaScript na stronie. Umożliwia podzielenie pakietu JavaScript na 2 części:
- JavaScript jest potrzebny podczas wczytywania strony, więc nie można go wczytać w innym czasie.
- Pozostały kod JavaScript, który można wczytać w późniejszym czasie, najczęściej w momencie, gdy użytkownik wchodzi w interakcję z danym elementem interaktywnym na stronie.
Dzielenie kodu można przeprowadzić za pomocą składni dynamicznego import()
. Ta składnia, w przeciwieństwie do elementów <script>
, które żądają danego zasobu JavaScript podczas uruchamiania, wysyła żądanie zasobu JavaScript w późniejszym momencie cyklu życia strony.
document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
// Get the form validation named export from the module through destructuring:
const { validateForm } = await import('/validate-form.mjs');
// Validate the form:
validateForm();
}, { once: true });
W powyższym fragmencie kodu JavaScript moduł validate-form.mjs
jest pobierany, analizowany i uruchamiany tylko wtedy, gdy użytkownik przeniesie fokus z dowolnego pola formularza <input>
. W takiej sytuacji zasób JavaScript odpowiedzialny za logikę weryfikacji formularza jest używany na stronie tylko wtedy, gdy jest to najbardziej prawdopodobne.
Usługi tworzące pakiety JavaScript, takie jak webpack, Parcel, Rollup i esbuild, można skonfigurować tak, aby dzieliły pakiety JavaScript na mniejsze części, gdy w kodzie źródłowym napotkają dynamiczne wywołanie import()
. Większość tych narzędzi robi to automatycznie, ale w przypadku esbuild musisz wyrazić zgodę na tę optymalizację.
Przydatne uwagi na temat dzielenia kodu
Dzielenie kodu to skuteczna metoda ograniczania rywalizacji w głównym wątku podczas początkowego wczytywania strony. Jeśli jednak zdecydujesz się sprawdzić kod źródłowy JavaScript pod kątem możliwości dzielenia kodu, warto pamiętać o kilku kwestiach.
Jeśli możesz, użyj narzędzia do łączenia plików
Deweloperzy często używają modułów JavaScript w procesie tworzenia aplikacji. To doskonałe ulepszenie, które zwiększa czytelność i łatwość utrzymania kodu. Wysyłanie modułów JavaScript do środowiska produkcyjnego może jednak powodować pewne nieoptymalne cechy wydajności.
Najważniejsze jest użycie narzędzia do łączenia plików, które przetworzy i zoptymalizuje kod źródłowy, w tym moduły, które chcesz podzielić. Narzędzia do łączenia plików są bardzo skuteczne nie tylko w stosowaniu optymalizacji do kodu źródłowego JavaScript, ale też w równoważeniu kwestii związanych z wydajnością, takich jak rozmiar pakietu, z współczynnikiem kompresji. Skuteczność kompresji rośnie wraz z rozmiarem pakietu, ale narzędzia do tworzenia pakietów starają się też, aby pakiety nie były tak duże, że powodują długie zadania z powodu oceny skryptu.
Bundlery pozwalają też uniknąć problemu z przesyłaniem przez sieć dużej liczby niepołączonych modułów. Architektury, które korzystają z modułów JavaScript, mają zwykle duże i złożone drzewa modułów. Gdy drzewa modułów nie są połączone w pakiet, każdy moduł reprezentuje osobne żądanie HTTP, a interaktywność w aplikacji internetowej może być opóźniona, jeśli nie połączysz modułów w pakiet. Chociaż można użyć <link rel="modulepreload">
wskazówki dotyczącej zasobów, aby wczytywać duże drzewa modułów tak wcześnie, jak to możliwe, z punktu widzenia wydajności wczytywania preferowane są nadal pakiety JavaScript.
Nie wyłączaj przypadkowo kompilacji strumieniowej
Mechanizm JavaScript V8 Chromium oferuje szereg gotowych optymalizacji, które zapewniają jak najbardziej efektywne wczytywanie kodu JavaScript w wersji produkcyjnej. Jedną z tych optymalizacji jest kompilacja strumieniowa, która – podobnie jak przyrostowa analiza składni kodu HTML przesyłanego strumieniowo do przeglądarki – kompiluje przesyłane strumieniowo fragmenty kodu JavaScript w miarę ich docierania z sieci.
Aby mieć pewność, że kompilacja strumieniowa będzie działać w przypadku Twojej aplikacji internetowej w Chromium, możesz wykonać te czynności:
- Przekształć kod produkcyjny, aby uniknąć używania modułów JavaScriptu. Bundlery mogą przekształcać kod źródłowy JavaScript na podstawie docelowego środowiska kompilacji, a to środowisko jest często specyficzne dla danego środowiska. V8 będzie stosować kompilację strumieniową do każdego kodu JavaScript, który nie używa modułów. Możesz skonfigurować narzędzie do łączenia, aby przekształcało kod modułu JavaScript w składnię, która nie używa modułów JavaScript ani ich funkcji.
- Jeśli chcesz wysyłać moduły JavaScriptu do środowiska produkcyjnego, użyj rozszerzenia
.mjs
. Niezależnie od tego, czy w produkcyjnym kodzie JavaScript używasz modułów, nie ma specjalnego typu treści dla JavaScriptu, który używa modułów, w porównaniu z JavaScriptem, który ich nie używa. W przypadku V8 rezygnujesz z kompilacji strumieniowej, gdy dostarczasz moduły JavaScript w środowisku produkcyjnym za pomocą rozszerzenia.js
. Jeśli używasz rozszerzenia.mjs
w przypadku modułów JavaScript, V8 może zapewnić, że kompilacja strumieniowa kodu JavaScript opartego na modułach nie zostanie przerwana.
Nie pozwól, aby te kwestie zniechęciły Cię do korzystania z podziału kodu. Dzielenie kodu to skuteczny sposób na zmniejszenie początkowych pakietów JavaScriptu wysyłanych do użytkowników, ale korzystając z pakietu i wiedząc, jak zachować działanie kompilacji strumieniowej V8, możesz mieć pewność, że kod JavaScript w wersji produkcyjnej będzie działać tak szybko, jak to możliwe.
Demo importu dynamicznego
webpack
webpack zawiera wtyczkę o nazwie SplitChunksPlugin
, która umożliwia skonfigurowanie sposobu dzielenia plików JavaScript przez narzędzie do łączenia. webpack rozpoznaje zarówno dynamiczne instrukcje import()
, jak i statyczne instrukcje import
. Działanie elementu SplitChunksPlugin
można zmodyfikować, określając w jego konfiguracji opcję chunks
:
chunks: async
to wartość domyślna, która odnosi się do dynamicznych wywołańimport()
.chunks: initial
odnosi się do statycznych wywołańimport
.chunks: all
obejmuje zarówno dynamiczneimport()
, jak i statyczne importy, co umożliwia udostępnianie fragmentów między importamiasync
iinitial
.
Domyślnie za każdym razem, gdy webpack napotka dynamiczną instrukcję import()
, tworzy dla tego modułu osobny fragment:
/* main.js */
// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';
myFunction('Hello world!');
// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
// Assumes top-level await is available. More info:
// https://v8.dev/features/top-level-await
await import('/form-validation.js');
}
Domyślna konfiguracja webpacka dla powyższego fragmentu kodu powoduje utworzenie 2 osobnych fragmentów:
- Fragment
main.js
, który webpack klasyfikuje jako fragmentinitial
, zawierający modułymain.js
i./my-function.js
. async
, który zawiera tylkoform-validation.js
(zawierający hash pliku w nazwie zasobu, jeśli jest skonfigurowany). Ten fragment jest pobierany tylko wtedy, gdy wartośćcondition
jest prawdziwa.
Ta konfiguracja pozwala odroczyć wczytywanie fragmentu form-validation.js
do momentu, gdy będzie on rzeczywiście potrzebny. Może to poprawić czas reakcji podczas wczytywania, skracając czas oceny skryptu podczas początkowego wczytywania strony. Pobieranie i ocena skryptu dla fragmentu form-validation.js
następuje po spełnieniu określonego warunku. W takim przypadku pobierany jest dynamicznie importowany moduł. Może to być na przykład sytuacja, w której polyfill jest pobierany tylko w przypadku konkretnej przeglądarki lub – jak w poprzednim przykładzie – zaimportowany moduł jest niezbędny do interakcji użytkownika.
Z drugiej strony zmiana konfiguracji SplitChunksPlugin
na chunks: initial
zapewnia podział kodu tylko na początkowe fragmenty. Są to fragmenty, takie jak te zaimportowane statycznie lub wymienione we właściwości entry
webpacka. W powyższym przykładzie wynikowy fragment będzie połączeniem form-validation.js
i main.js
w jednym pliku skryptu, co może pogorszyć wydajność początkowego wczytywania strony.
Opcje SplitChunksPlugin
można też skonfigurować tak, aby większe skrypty były dzielone na kilka mniejszych, np. za pomocą opcji maxSize
, która nakazuje webpackowi dzielenie fragmentów na osobne pliki, jeśli przekraczają one wartość określoną przez maxSize
. Podzielenie dużych plików skryptów na mniejsze może poprawić szybkość ładowania, ponieważ w niektórych przypadkach wymagające dużych zasobów procesora zadania związane z oceną skryptu są dzielone na mniejsze zadania, które rzadziej blokują główny wątek na dłuższy czas.
Generowanie większych plików JavaScript oznacza też, że skrypty są bardziej podatne na unieważnienie pamięci podręcznej. Jeśli na przykład wyślesz bardzo duży skrypt zawierający zarówno kod platformy, jak i kod aplikacji własnej, cały pakiet może zostać unieważniony, jeśli zaktualizowana zostanie tylko platforma, a nie inne elementy w zasobie pakietu.
Z drugiej strony mniejsze pliki skryptów zwiększają prawdopodobieństwo, że powracający użytkownik pobierze zasoby z pamięci podręcznej, co przyspieszy wczytywanie strony podczas kolejnych wizyt. Mniejsze pliki są jednak mniej podatne na kompresję niż większe, a w przypadku wczytywania stron z nieprzygotowaną pamięcią podręczną przeglądarki mogą wydłużać czas podróży w obie strony w sieci. Należy zachować równowagę między wydajnością buforowania, skutecznością kompresji i czasem oceny skryptu.
Wersja demonstracyjna webpack
webpack SplitChunksPlugin
demo
Sprawdź swoją wiedzę
Jakiego typu instrukcji import
używa się podczas dzielenia kodu?
import()
.import
Który typ import
instrukcjimusi znajdować się na początku modułu JavaScript i w żadnym innym miejscu?
import()
.import
Jaką różnicę w webpacku robi użycie SplitChunksPlugin
w przypadku fragmentu async
i fragmentu initial
?
async
są ładowane dynamicznie, a import()
initial
są ładowane statycznieimport
.
async
są ładowane statycznie, a initial
dynamicznieimport()
.import
Dalej: leniwe ładowanie obrazów i elementów <iframe>
JavaScript jest dość kosztownym typem zasobu, ale nie jest jedynym, którego wczytywanie można odłożyć. Obrazy i <iframe>
elementy mogą być kosztownymi zasobami. Podobnie jak w przypadku JavaScriptu możesz opóźnić wczytywanie obrazów i elementu <iframe>
, wczytując je z opóźnieniem. Wyjaśnimy to w następnym module tego kursu.