Ocena skryptu i długie zadania

Podczas wczytywania skryptów przeglądarka potrzebuje czasu na ich ocenę przed wykonaniem, co może powodować długie zadania. Dowiedz się, jak działa ocena skryptu i co możesz zrobić, aby zapobiec powstawaniu długich zadań podczas wczytywania strony.

Jeśli chodzi o optymalizację interakcji do kolejnego wyrenderowania (INP), większość porad dotyczy optymalizacji samych interakcji. Na przykład w przewodniku po optymalizacji długich zadań omawiamy techniki takie jak przekazywanie sterowania za pomocą setTimeout. Te techniki są korzystne, ponieważ pozwalają wątkowi głównemu na odciążenie poprzez unikanie długich zadań, co może zwiększyć możliwości interakcji i innej aktywności, które mogą być wykonywane szybciej, niż gdyby musiały czekać na jedno długie zadanie.

A co z długimi zadaniami wynikającymi z wczytywania skryptów? Te zadania mogą zakłócać interakcje użytkownika i wpływać na INP strony podczas wczytywania. Z tego przewodnika dowiesz się, jak przeglądarki obsługują zadania uruchamiane przez ocenę skryptu, oraz co możesz zrobić, aby podzielić pracę związaną z oceną skryptu, tak aby główny wątek mógł lepiej reagować na dane wejściowe użytkownika podczas ładowania strony.

Na czym polega ocena skryptu?

Jeśli profilujesz aplikację, która wysyła dużo kodu JavaScript, mogłeś(-aś) zauważyć długie zadania, w których przyczyną jest Evaluate Script.

Praca związana z oceną skryptu wizualizowana w profilerze wydajności w Narzędziach deweloperskich w Chrome. Praca powoduje długie zadanie podczas uruchamiania, które blokuje możliwość reagowania wątku głównego na interakcje użytkownika.
Praca związana z oceną skryptu, jak pokazano w profilerze wydajności w Narzędziach deweloperskich w Chrome. W takim przypadku praca jest wystarczająca, aby spowodować długie zadanie, które uniemożliwia głównemu wątkowi wykonywanie innych zadań, w tym tych, które wpływają na interakcje użytkownika.

Ocena skryptu jest niezbędnym elementem wykonywania JavaScriptu w przeglądarce, ponieważ JavaScript jest kompilowany bezpośrednio przed wykonaniem. Gdy skrypt jest oceniany, najpierw jest analizowany pod kątem błędów. Jeśli parser nie znajdzie błędów, skrypt jest kompilowany do kodu bajtowego, a następnie może być wykonywany.

Ocena skryptu jest niezbędna, ale może być problematyczna, ponieważ użytkownicy mogą próbować korzystać ze strony krótko po jej pierwszym wyrenderowaniu. To, że strona została wyrenderowana, nie oznacza jednak, że została załadowana. Interakcje, które mają miejsce podczas ładowania, mogą być opóźnione, ponieważ strona jest zajęta ocenianiem skryptów. Chociaż nie ma gwarancji, że w tym momencie może dojść do interakcji (odpowiedzialny za nią skrypt mógł jeszcze nie zostać wczytany), mogą istnieć interakcje zależne od JavaScriptu, które gotowe, lub interaktywność wcale nie zależy od JavaScriptu.

Związek między skryptami a zadaniami, które je oceniają

Sposób uruchamiania zadań odpowiedzialnych za ocenę skryptu zależy od tego, czy wczytywany skrypt jest wczytywany za pomocą typowego elementu <script>, czy jest modułem wczytywanym za pomocą elementu type=module. Przeglądarki mają tendencję do różnego traktowania pewnych kwestii, dlatego w miejscach, w których zachowania związane z oceną skryptu różnią się w przypadku głównych silników przeglądarek, omówimy sposób, w jaki te silniki oceniają skrypty.

Skrypty załadowane za pomocą elementu <script>

Liczba zadań wysyłanych do oceny skryptów jest zwykle bezpośrednio powiązana z liczbą elementów <script> na stronie. Każdy element <script> uruchamia zadanie oceny żądanego skryptu, aby można go było przeanalizować, skompilować i wykonać. Dotyczy to przeglądarek opartych na Chromium, Safari i Firefoxa.

Dlaczego ma to znaczenie? Załóżmy, że używasz narzędzia do łączenia plików, aby zarządzać skryptami produkcyjnymi, i skonfigurowano je tak, aby łączyło wszystko, czego strona potrzebuje do działania, w jeden skrypt. Jeśli tak jest w przypadku Twojej witryny, możesz się spodziewać, że do oceny tego skryptu zostanie wysłane jedno zadanie. Czy to źle? Niekoniecznie, chyba że skrypt jest bardzo duży.

Możesz podzielić pracę związaną z oceną skryptu, unikając wczytywania dużych fragmentów JavaScriptu i wczytując więcej pojedynczych, mniejszych skryptów za pomocą dodatkowych elementów <script>.

Podczas wczytywania strony zawsze staraj się wczytywać jak najmniej kodu JavaScript. Podzielenie skryptów sprawi, że zamiast jednego dużego zadania, które może blokować wątek główny, będziesz mieć więcej mniejszych zadań, które nie będą go blokować wcale lub będą to robić w mniejszym stopniu niż wcześniej.

Wiele zadań związanych z oceną skryptu, które są wizualizowane w profilerze wydajności Narzędzi deweloperskich w Chrome. Ponieważ wczytywanych jest wiele mniejszych skryptów zamiast mniejszej liczby większych skryptów, zadania rzadziej stają się długimi zadaniami, co pozwala głównemu wątkowi szybciej reagować na dane wejściowe użytkownika.
Wiele zadań uruchomionych w celu oceny skryptów w wyniku obecności wielu elementów <script> w kodzie HTML strony. Jest to lepsze rozwiązanie niż wysyłanie użytkownikom jednego dużego pakietu skryptów, który z większym prawdopodobieństwem zablokuje wątek główny.

Podział zadań na potrzeby oceny skryptu jest podobny do przekazywania sterowania podczas wywołań zwrotnych zdarzeń, które są wykonywane w trakcie interakcji. W przypadku oceny skryptu mechanizm przekazywania dzieli wczytywany kod JavaScript na wiele mniejszych skryptów, a nie na mniejszą liczbę większych skryptów, które częściej blokują wątek główny.

Skrypty wczytywane za pomocą elementu <script> i atrybutu type=module

Teraz możesz natywnie wczytywać moduły ES w przeglądarce za pomocą atrybutu type=module w elemencie <script>. Takie podejście do wczytywania skryptów ma pewne korzyści z punktu widzenia programisty, np. nie trzeba przekształcać kodu do użytku produkcyjnego, zwłaszcza w połączeniu z mapami importu. Jednak wczytywanie skryptów w ten sposób powoduje zaplanowanie zadań, które różnią się w zależności od przeglądarki.

Przeglądarki oparte na Chromium

W przeglądarkach takich jak Chrome lub w przeglądarkach z niej wywodzących się wczytywanie modułów ES za pomocą atrybutu type=module powoduje powstawanie innych rodzajów zadań niż w przypadku, gdy nie używasz atrybutu type=module. Na przykład dla każdego skryptu modułu zostanie uruchomione zadanie obejmujące aktywność oznaczoną jako Kompilacja modułu.

Praca kompilacji modułu w wielu zadaniach, jak widać w narzędziach deweloperskich w Chrome.
Zachowanie podczas wczytywania modułów w przeglądarkach opartych na Chromium. Każdy skrypt modułu wywoła funkcję Compile module, aby skompilować swoją zawartość przed jej oceną.

Po skompilowaniu modułów każdy kod, który będzie w nich później uruchamiany, spowoduje rozpoczęcie działania oznaczonego jako Evaluate module (Ocena modułu).

Ocena modułu w odpowiednim momencie wizualizowana w panelu wydajności Narzędzi deweloperskich w Chrome.
Gdy kod w module jest uruchamiany, moduł jest oceniany w odpowiednim momencie.

W Chrome i powiązanych przeglądarkach efekt jest taki, że w przypadku modułów ES etapy kompilacji są rozdzielane. To niewątpliwa zaleta w przypadku zarządzania długimi zadaniami. Jednak praca związana z oceną modułu nadal wiąże się z pewnymi nieuniknionymi kosztami. Chociaż należy dążyć do wysyłania jak najmniejszej ilości kodu JavaScript, używanie modułów ES – niezależnie od przeglądarki – zapewnia następujące korzyści:

  • Cały kod modułu jest automatycznie uruchamiany w trybie ścisłym, co umożliwia potencjalne optymalizacje przez silniki JavaScript, które w innych przypadkach nie byłyby możliwe.
  • Skrypty wczytywane za pomocą type=module są domyślnie traktowane jako odroczone. Aby zmienić to zachowanie, możesz użyć atrybutu async w skryptach wczytywanych za pomocą atrybutu type=module.

Safari i Firefox

Gdy moduły są wczytywane w Safari i Firefoxie, każdy z nich jest oceniany w osobnym zadaniu. Oznacza to, że teoretycznie możesz załadować do innych modułów pojedynczy moduł najwyższego poziomu składający się tylko z instrukcji statycznychimport, a każdy załadowany moduł spowoduje oddzielne żądanie sieciowe i zadanie związane z jego oceną.

Skrypty wczytywane za pomocą dynamicznego tagu import()

Dynamiczne import() to kolejna metoda wczytywania skryptów. W przeciwieństwie do statycznych instrukcji import, które muszą znajdować się na początku modułu ES, dynamiczne wywołanie import() może pojawić się w dowolnym miejscu skryptu, aby wczytać fragment JavaScriptu na żądanie. Ta technika nazywa się dzieleniem kodu.

Dynamiczne import() ma 2 zalety, jeśli chodzi o poprawę INP:

  1. Moduły, których wczytywanie jest odroczone, zmniejszają rywalizację o główny wątek podczas uruchamiania, ponieważ zmniejszają ilość wczytywanego w tym czasie kodu JavaScript. Dzięki temu główny wątek jest bardziej responsywny w odniesieniu do interakcji użytkowników.
  2. Gdy wykonywane są dynamiczne wywołania import(), każde wywołanie skutecznie rozdziela kompilację i ocenę każdego modułu na osobne zadania. Oczywiście dynamiczny tag import(), który wczytuje bardzo duży moduł, uruchomi dość duże zadanie oceny skryptu, co może utrudnić wątkowi głównemu reagowanie na dane wejściowe użytkownika, jeśli interakcja nastąpi w tym samym czasie co wywołanie dynamicznego tagu import(). Dlatego nadal bardzo ważne jest, aby ładować jak najmniej kodu JavaScript.

Dynamiczne wywołania import() działają podobnie we wszystkich głównych silnikach przeglądarek: zadania oceny skryptu, które z nich wynikają, będą takie same jak liczba modułów importowanych dynamicznie.

Skrypty wczytywane w procesie roboczym

Web workers to specjalny przypadek użycia JavaScriptu. Web Worker jest rejestrowany w wątku głównym, a kod w nim zawarty jest wykonywany w osobnym wątku. Jest to bardzo korzystne, ponieważ kod rejestrujący web workera działa w głównym wątku, ale kod w web workerze już nie. Zmniejsza to obciążenie głównego wątku i może sprawić, że będzie on lepiej reagować na interakcje użytkowników.

Oprócz zmniejszania obciążenia głównego wątku instancje robocze mogą samodzielnie wczytywać skrypty zewnętrzne do użycia w kontekście instancji roboczej za pomocą importScripts lub statycznych instrukcji import w przeglądarkach obsługujących instancje robocze modułów. W rezultacie każdy skrypt, o który poprosi instancja robocza, jest wykonywany poza głównym wątkiem.

Kompromisy i kwestie do rozważenia

Podzielenie skryptów na mniejsze, osobne pliki pomaga ograniczyć długie zadania w przeciwieństwie do wczytywania mniejszej liczby znacznie większych plików. Przy podejmowaniu decyzji o tym, jak podzielić skrypty, warto wziąć pod uwagę kilka kwestii.

Wydajność kompresji

Kompresja jest czynnikiem, który należy wziąć pod uwagę przy dzieleniu skryptów. Gdy skrypty są mniejsze, kompresja staje się nieco mniej wydajna. Większe skrypty przyniosą znacznie więcej korzyści z kompresji. Zwiększenie skuteczności kompresji pomaga utrzymać jak najkrótszy czas wczytywania skryptów, ale trzeba zachować równowagę, aby podzielić skrypty na wystarczająco małe części, co ułatwi interaktywność podczas uruchamiania.

Bundlery to idealne narzędzia do zarządzania rozmiarem wyjściowym skryptów, od których zależy Twoja witryna:

  • W przypadku webpacka może pomóc wtyczka SplitChunksPlugin. Opcje, które możesz ustawić, aby zarządzać rozmiarami komponentów, znajdziesz w SplitChunksPlugindokumentacji.
  • W przypadku innych narzędzi do łączenia plików, takich jak Rollupesbuild, możesz zarządzać rozmiarami plików skryptów, używając w kodzie dynamicznych wywołań import(). Te narzędzia do łączenia plików, a także webpack, automatycznie wyodrębniają dynamicznie importowany zasób do osobnego pliku, co pozwala uniknąć większych rozmiarów początkowego pakietu.

Unieważnienie pamięci podręcznej

Unieważnianie pamięci podręcznej odgrywa dużą rolę w szybkości wczytywania strony podczas ponownych wizyt. Wysyłanie dużych, monolitycznych pakietów skryptów jest niekorzystne z punktu widzenia zapisywania w pamięci podręcznej przeglądarki. Dzieje się tak, ponieważ po zaktualizowaniu kodu własnego (przez zaktualizowanie pakietów lub wprowadzenie poprawek błędów) cały pakiet staje się nieważny i musi zostać pobrany ponownie.

Dzieląc skrypty, nie tylko rozdzielasz pracę związaną z oceną skryptu na mniejsze zadania, ale też zwiększasz prawdopodobieństwo, że powracający użytkownicy pobiorą więcej skryptów z pamięci podręcznej przeglądarki zamiast z sieci. Przekłada się to na ogólnie szybsze wczytywanie strony.

Zagnieżdżone moduły i wydajność wczytywania

Jeśli w środowisku produkcyjnym wysyłasz moduły ES i wczytujesz je za pomocą atrybutu type=module, musisz wiedzieć, jak zagnieżdżanie modułów może wpływać na czas uruchamiania. Zagnieżdżanie modułów to sytuacja, w której moduł ES statycznie importuje inny moduł ES, który statycznie importuje kolejny moduł ES:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Jeśli moduły ES nie są połączone w pakiet, powyższy kod powoduje powstanie łańcucha żądań sieciowych: gdy z elementu <script> zostanie wysłane żądanie a.js, wysyłane jest kolejne żądanie sieciowe dotyczące b.js, które następnie obejmuje kolejne żądanie dotyczące c.js. Jednym ze sposobów na uniknięcie tego problemu jest użycie narzędzia do łączenia plików, ale pamiętaj, aby skonfigurować je tak, aby dzieliło skrypty i rozdzielało pracę związaną z ich oceną.

Jeśli nie chcesz używać narzędzia do łączenia plików, możesz obejść wywołania zagnieżdżonych modułów, korzystając ze modulepreload wskazówki dotyczącej zasobów, która wstępnie wczyta moduły ES, aby uniknąć łańcuchów żądań sieciowych.

Podsumowanie

Optymalizacja oceny skryptów w przeglądarce jest niewątpliwie trudnym zadaniem. Podejście zależy od wymagań i ograniczeń witryny. Dzieląc skrypty, rozkładasz jednak pracę związaną z ich oceną na wiele mniejszych zadań, dzięki czemu główny wątek może wydajniej obsługiwać interakcje użytkownika, zamiast blokować główny wątek.

Podsumowując, oto kilka rzeczy, które możesz zrobić, aby podzielić duże zadania związane z oceną skryptu:

  • Podczas wczytywania skryptów za pomocą elementu <script> bez atrybutu type=module unikaj wczytywania bardzo dużych skryptów, ponieważ uruchamiają one zadania oceny skryptów wymagające dużej ilości zasobów, które blokują wątek główny. Rozłóż skrypty na większą liczbę elementów <script>, aby podzielić pracę.
  • Użycie atrybutu type=module do natywnego wczytywania modułów ES w przeglądarce spowoduje uruchomienie poszczególnych zadań oceny dla każdego oddzielnego skryptu modułu.
  • Zmniejsz rozmiar początkowych pakietów, używając dynamicznych wywołań import(). Działa to również w przypadku narzędzi do łączenia plików, ponieważ traktują one każdy dynamicznie importowany moduł jako „punkt podziału”, co powoduje wygenerowanie osobnego skryptu dla każdego dynamicznie importowanego modułu.
  • Pamiętaj, aby rozważyć kompromisy, takie jak wydajność kompresji i unieważnianie pamięci podręcznej. Większe skrypty będą lepiej kompresowane, ale częściej będą wymagać kosztownej oceny skryptu w mniejszej liczbie zadań i powodować unieważnienie pamięci podręcznej przeglądarki, co prowadzi do ogólnie niższej wydajności buforowania.
  • Jeśli używasz modułów ES natywnie bez pakowania, użyj wskazówki dotyczącej zasobów modulepreload, aby zoptymalizować ich wczytywanie podczas uruchamiania.
  • Jak zawsze, wysyłaj jak najmniej kodu JavaScript.

To z pewnością wymaga zachowania równowagi, ale dzieląc skrypty i zmniejszając początkowe ładunki za pomocą dynamicznego import(), możesz uzyskać lepszą wydajność uruchamiania i lepiej dostosować się do interakcji użytkowników w tym kluczowym okresie. Powinno to pomóc Ci uzyskać lepszy wynik w przypadku wskaźnika INP, a tym samym zapewnić użytkownikom lepsze wrażenia.