JavaScript z podziałem kodu

Wczytywanie dużych zasobów JavaScript znacznie spowalnia działanie stron. Dzielenie kodu JavaScript na mniejsze fragmenty i pobieranie tylko tych elementów, które są niezbędne do poprawnego funkcjonowania strony podczas uruchamiania, może znacznie poprawić jej elastyczność ładowania, co z kolei może poprawić jej interakcje z programem NextPaint (INP).

Gdy strona pobiera, analizuje i kompiluje duże pliki JavaScript, przez dłuższy czas może przestać reagować. Elementy strony są widoczne, ponieważ stanowią część początkowego kodu HTML strony i mają styl CSS. Ponieważ jednak JavaScript wymagany do obsługi tych elementów interaktywnych, a także inne skrypty wczytywane przez stronę, może analizować i wykonywać kod JavaScript, aby mogły działać. W efekcie użytkownik może mieć wrażenie, że interakcja była znacznie opóźniona, a nawet całkowicie uszkodzona.

Dzieje się tak często, gdy wątek główny jest zablokowany, a JavaScript jest analizowany i kompilowany w wątku głównym. Jeśli ten proces będzie zbyt długi, interaktywne elementy strony mogą nie reagować odpowiednio szybko na działania użytkownika. Jednym z rozwiązań tego problemu jest wczytywanie tylko tego kodu JavaScriptu, który jest potrzebny do działania strony, a jednocześnie odłożenie tego kodu na późniejszy mechanizm za pomocą metody znanej jako dzielenie kodu. Ten moduł dotyczy drugiej z tych metod.

Ogranicz analizowanie i wykonywanie kodu JavaScript podczas uruchamiania przez podział kodu

Lighthouse zwraca ostrzeżenie, gdy wykonanie JavaScriptu trwa dłużej niż 2 sekundy, oraz kończy się niepowodzeniem, jeśli trwa dłużej niż 3,5 sekundy. Nadmierne analizowanie i wykonywanie kodu JavaScript to potencjalny problem w dowolnym momencie cyklu życia strony, ponieważ może zwiększyć opóźnienie danych wejściowych przed interakcją, jeśli czas interakcji użytkownika ze stroną zbiega się z możliwością wykonania głównych zadań w wątku odpowiedzialnych za przetwarzanie i wykonywanie kodu JavaScript.

Poza tym nadmierna liczba wykonywania i analizowania JavaScriptu jest szczególnie problematyczna przy początkowym wczytywaniu strony, ponieważ to na tym etapie cyklu życia strony użytkownicy z dużym prawdopodobieństwem będą z niej korzystać. Całkowity czas blokowania (TBT) – wskaźnik czasu reakcji na wczytywanie – jest silnie skorelowany z INP, co wskazuje, że użytkownicy są bardzo skłonni do podejmowania prób interakcji już podczas wstępnego wczytywania strony.

Audyt Lighthouse, który pokazuje czas wykonania każdego pliku JavaScript żądania strony, jest przydatny, ponieważ pomaga dokładnie określić, które skrypty nadają się do podziału kodu. Możesz też skorzystać z narzędzia pokrycia w Narzędziach deweloperskich w Chrome, aby dokładnie określić, które części kodu JavaScript strony nie są używane podczas wczytywania strony.

Podział kodu to przydatna technika, która pozwala zmniejszyć początkowe ładunki JavaScriptu na stronie. Pozwala podzielić pakiet JavaScript na 2 części:

  • Kod JavaScript jest wymagany podczas wczytywania strony i dlatego nie można go wczytać w innym momencie.
  • Pozostały kod JavaScript, który może zostać wczytany w późniejszym czasie, najczęściej w momencie interakcji użytkownika z danym elementem interaktywnym na stronie.

Podział kodu można zastosować, korzystając ze składni dynamicznej import(). Ta składnia – w przeciwieństwie do elementów <script>, które podczas uruchamiania żądają określonego zasobu JavaScriptu – powoduje wysłanie żądania zasobu JavaScript na późniejszym etapie 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 poprzednim fragmencie kodu JavaScript moduł validate-form.mjs jest pobierany, analizowany i wykonywany tylko wtedy, gdy użytkownik zamazuje dowolne z pól <input> formularza. W takiej sytuacji zasób JavaScript odpowiedzialny za obsługę logiki weryfikacji formularza pojawia się na stronie tylko wtedy, gdy jest najbardziej prawdopodobne, że zostanie faktycznie użyta.

Programy tworzenia pakietów JavaScript, np. webpack, Parcel, Rollup i esbuild, można skonfigurować tak, aby dzielić pakiety JavaScript na mniejsze fragmenty za każdym razem, gdy w kodzie źródłowym występuje dynamiczne wywołanie import(). Większość z tych narzędzi robi to automatycznie, ale w szczególności musisz włączyć tę optymalizację.

Przydatne uwagi na temat podziału kodu

Podział kodu jest skuteczną metodą zmniejszenia rywalizacji o wątek główny podczas początkowego wczytywania strony. Warto jednak pamiętać o kilku kwestiach, jeśli zdecydujesz się sprawdzić kod źródłowy JavaScript pod kątem możliwości podziału kodu.

Jeśli możesz, używaj usługi tworzenia pakietów

W procesie programowania deweloperzy często używają modułów JavaScript. Jest to doskonałe narzędzie dla programistów, które poprawia czytelność i łatwość obsługi kodu. Istnieją jednak pewne nieoptymalne cechy wydajności, które mogą spowodować przesyłanie modułów JavaScript do środowiska produkcyjnego.

Przede wszystkim do przetwarzania i optymalizowania kodu źródłowego, łącznie z modułami, które zamierzasz podzielić, używaj programu do tworzenia pakietów. Bundle są bardzo skuteczne nie tylko w optymalizacji kodu źródłowego JavaScript, ale także w równoważeniu wydajności, np. rozmiaru pakietu i współczynnika kompresji. Skuteczność kompresji rośnie wraz z rozmiarem pakietu, ale osoby tworzące pakiety starają się zadbać o to, aby pakiety nie były tak duże, aby wykonywanie długich zadań z powodu oceny skryptów.

Dzięki pakietom można też uniknąć problemu przesyłania przez sieć dużej liczby niegrupowanych modułów. Budynki, które korzystają z modułów JavaScript, mają zwykle duże i złożone drzewa modułów. Gdy drzewa modułów są rozgrupowane, każdy moduł reprezentuje osobne żądanie HTTP, a w przeciwnym razie interaktywność aplikacji internetowej może być opóźniona. Można korzystać ze wskaźnika dotyczącego zasobów <link rel="modulepreload"> do jak najszybszego wczytywania dużych drzew modułów, jednak ze względu na wydajność wczytywania nadal preferujemy pakiety JavaScript.

Nie wyłączaj kompilacji strumieniowania przez przypadek

Mechanizm JavaScript V8 w Chromium od razu udostępnia wiele funkcji optymalizacji, dzięki którym produkcyjny kod JavaScript wczytuje się z maksymalną wydajnością. Jedna z tych optymalizacji jest nazywana kompilacją strumieniową, która – podobnie jak przyrostowa analiza kodu HTML przesyłanego do przeglądarki – kompiluje przesyłane strumieniowo fragmenty kodu JavaScript pobierane z sieci.

Kompilację aplikacji internetowej możesz sprawdzić w Chromium na kilka sposobów:

  • Przekształć kod produkcyjny, aby uniknąć używania modułów JavaScript. Pakiety mogą przekształcać kod źródłowy JavaScript na podstawie celu kompilacji, a cele często są specyficzne dla danego środowiska. Wersja 8 stosuje kompilację strumieniową do każdego kodu JavaScript, który nie korzysta z modułów. Program ten można skonfigurować tak, aby przekształcał kod modułu JavaScript w składnię bez modułów JavaScript ani ich funkcji.
  • Jeśli chcesz wysyłać moduły JavaScript do środowiska produkcyjnego, użyj rozszerzenia .mjs. Niezależnie od tego, czy w środowisku JavaScript używasz modułów, nie ma specjalnego typu treści dla JavaScriptu, który używa modułów, i tego, który go nie używa. W przypadku wersji 8 rezygnujesz z kompilacji strumieniowania, przesyłając moduły JavaScript w wersji produkcyjnej za pomocą rozszerzenia .js. Jeśli używasz rozszerzenia .mjs na potrzeby modułów JavaScript, wersja 8 może zagwarantować, że kompilacja strumieniowa kodu JavaScript opartego na modułach nie zostanie uszkodzona.

Niech te kwestie zniechęcą Cię do stosowania podziału kodu. Dzielenie kodu to efektywny sposób zmniejszenia początkowych ładunków JavaScript u użytkowników. Jeśli jednak będziesz korzystać z narzędzia do tworzenia pakietów i wiedząc, jak zachować kompilację strumieniowania V8, możesz zadbać o to, aby produkcyjny kod JavaScript był maksymalnie szybki dla użytkowników.

Prezentacja dynamicznego importu

pakiet internetowy

webpack – zawiera wtyczkę o nazwie SplitChunksPlugin, która pozwala skonfigurować sposób dzielenia plików JavaScript przez pakiet SDK. Webpack rozpoznaje zarówno dynamiczne instrukcje import(), jak i statyczne instrukcje import. Działanie SplitChunksPlugin można zmienić, określając w jego konfiguracji opcję chunks:

  • chunks: async to wartość domyślna, która odnosi się do dynamicznych wywołań import().
  • Pole chunks: initial odnosi się do statycznych wywołań funkcji import.
  • chunks: all obejmuje zarówno dynamiczne importu import(), jak i importy statyczne, co umożliwia współdzielenie fragmentów między importami od async do initial.

Domyślnie, gdy pakiet internetowy napotka dynamiczną instrukcję import(), tworzy oddzielny fragment dla tego modułu:

/* 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 pakietu internetowego dla poprzedniego fragmentu kodu powoduje utworzenie 2 osobnych fragmentów:

  • Fragment main.js, który jest klasyfikowany jako fragment initial, zawierający moduł main.js i ./my-function.js.
  • Fragment async, który zawiera tylko form-validation.js (zawiera hasz pliku w nazwie zasobu, jeśli jest skonfigurowany). Ten fragment jest pobierany tylko wtedy, gdy parametr condition ma wartość prawda.

Ta konfiguracja umożliwia odroczenie wczytywania fragmentu form-validation.js do momentu, gdy będzie on faktycznie potrzebny. Może to poprawić czas wczytywania, skracając czas oceny skryptu podczas wstępnego wczytywania strony. Pobieranie skryptu i ocena fragmentu form-validation.js ma miejsce, gdy zostanie spełniony określony warunek. W takim przypadku dynamicznie importowany moduł jest pobierany. Przykładem może być sytuacja, w której kod polyfill jest pobierany tylko dla określonej przeglądarki lub – jak we wcześniejszym przykładzie – zaimportowany moduł jest niezbędny do interakcji użytkownika.

Z drugiej strony zmiana konfiguracji SplitChunksPlugin na chunks: initial zapewni, że kod zostanie podzielony tylko na początkowe fragmenty. Są to fragmenty, np. zaimportowane statycznie lub wymienione we właściwości entry pakietu webowego. W poprzednim przykładzie wynikowy fragment stanowiłby kombinację elementów form-validation.js i main.js w jednym pliku skryptu, co potencjalnie pogorszyłoby początkowe wczytywanie strony.

Opcje SplitChunksPlugin można też skonfigurować tak, aby podzielić większe skrypty na kilka mniejszych. Możesz na przykład użyć opcji maxSize, aby nakazać pakietowi internetowemu dzielenie fragmentów na osobne pliki, jeśli przekraczają one wartość określoną przez maxSize. Podzielenie dużych plików skryptów na mniejsze pliki może poprawić ich responsywność, ponieważ w niektórych przypadkach ocena skryptów intensywnie obciążająca procesor jest dzielona na mniejsze zadania, które rzadziej blokują główny wątek na dłuższy czas.

Poza tym generowanie większych plików JavaScript zwiększa ryzyko unieważnienia pamięci podręcznej przez skrypty. Jeśli na przykład wysyłasz bardzo duży skrypt z kodem platformy i własnej aplikacji, cały pakiet może zostać unieważniony, gdy tylko platforma zostanie zaktualizowana, a nie nic innego w pakiecie.

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 przy kolejnych wizytach. Mniejsze pliki są jednak mniej kompresowane niż większe i mogą wydłużyć czas przesyłania danych w obie strony podczas wczytywania stron przy braku pamięci podręcznej przeglądarki. Trzeba zadbać o równowagę między wydajnością buforowania, skutecznością kompresji i czasem oceny skryptu.

wersja demonstracyjna pakietu internetowego

prezentacja pakietu internetowego SplitChunksPlugin.

Sprawdź swoją wiedzę

Jakiego typu instrukcji import używa się podczas podziału kodu?

Dynamiczna wartość import().
Dobrze!
Statyczna wartość import.
Spróbuj ponownie.

Który typ instrukcji import musi znajdować się u góry modułu JavaScript i w żadnym innym miejscu?

Dynamiczna wartość import().
Spróbuj ponownie.
Statyczna wartość import.
Dobrze!

Jaka jest różnica między fragmentem async a initial przy użyciu elementu SplitChunksPlugin w pakiecie internetowym?

async fragmenty są wczytywane za pomocą dynamicznego import(), a initial fragmenty są wczytywane za pomocą statycznego parametru import.
Dobrze!
async fragmentów wczytuje się za pomocą statycznego elementu import, a initial fragmentów – za pomocą dynamicznego import().
Spróbuj ponownie.

Następny krok: leniwe ładowanie obrazów i elementów <iframe>

Chociaż jest to dość kosztowny rodzaj zasobu, JavaScript nie jest jedynym, którego można odroczyć ładowanie. Obrazy i elementy <iframe> samodzielnie stanowią potencjalnie kosztowne zasoby. Podobnie jak w przypadku JavaScriptu, możesz opóźnić wczytywanie obrazów i elementu <iframe>, korzystając z leniwego ładowania. Zostało to opisane w następnym module tego kursu.