Ocena skryptu i długie zadania

Podczas wczytywania skryptów przeglądarka musi je przeanalizować przed wykonaniem, co może wydłużać czas wykonywania zadań. Dowiedz się, jak działa ocena skryptu i co możesz zrobić, aby nie powodowała długich zadań podczas wczytywania strony.

W przypadku optymalizacji czasu od interakcji do kolejnego wyrenderowania (INP) najczęściej spotykaną radą jest optymalizacja samych interakcji. Na przykład w przewodniku dotyczącym optymalizacji długich zadań omawiane są takie techniki jak setTimeout. Te techniki są korzystne, ponieważ dają wątkowi głównemu trochę miejsca na oddech, unikając długich zadań, co może zwiększyć możliwości interakcji i innych działań, które mogą być wykonywane szybciej niż w przypadku oczekiwania na jedno długie zadanie.

A co z długotrwałymi zadaniami, które wynikają z wczytywania skryptów? Te zadania mogą zakłócać interakcje z użytkownikiem i wpływać na INP strony podczas wczytywania. Z tego przewodnika dowiesz się, jak przeglądarki obsługują zadania uruchamiane przez interpretację skryptu, oraz jak możesz podzielić proces interpretacji skryptu, aby główny wątek był bardziej responsywny na dane wprowadzane przez użytkownika podczas wczytywania strony.

Co to jest ocena skryptu?

Jeśli przeprowadziłeś profilowanie aplikacji, która zawiera dużo kodu JavaScript, mogłeś zauważyć długie zadania, których sprawca ma etykietę Evaluate Script (Sprawdzanie skryptu).

Praca związana z ocenianiem skryptu, jak widać w profilu wydajności w Narzędziach deweloperskich w Chrome. Podczas uruchamiania aplikacja wykonuje długie zadanie, które uniemożliwia wątkowi głównemu reagowanie na interakcje użytkownika.
Sprawdzanie skryptu, jak pokazano w profilu wydajności w Narzędziach deweloperskich w Chrome. W tym przypadku praca jest wystarczająca, aby spowodować długie zadanie, które blokuje wątek główny przed wykonywaniem innych zadań, w tym zadań, które umożliwiają interakcje z użytkownikiem.

Ocenianie skryptu jest niezbędnym elementem wykonywania kodu JavaScript w przeglądarce, ponieważ jest on kompilowany tuż przed wykonaniem. Podczas oceny skryptu jest on najpierw analizowany pod kątem błędów. Jeśli parsujący nie znajdzie błędów, skrypt zostanie skompilowany w bajtkod, a następnie może zostać wykonany.

Chociaż jest to konieczne, ocena skryptu może być problematyczna, ponieważ użytkownicy mogą próbować wchodzić w interakcję ze stroną tuż po jej renderowaniu. Jednak to, że strona została wyrenderowana, nie oznacza, że została już w pełni wczytana. Interakcje występujące podczas wczytywania mogą być opóźnione, ponieważ strona jest zajęta wykonywaniem skryptów. Chociaż nie ma gwarancji, że interakcja może nastąpić w danym momencie, ponieważ skrypt odpowiedzialny za nią może nie być jeszcze załadowany, mogą wystąpić interakcje zależne od JavaScriptu, które gotowe, lub interaktywność może wcale nie zależeć 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 wczytywanie skryptu odbywa się za pomocą typowego elementu <script>, czy skrypt jest modułem wczytywanym za pomocą elementu type=module. Ponieważ przeglądarki mają tendencję do odmiennego przetwarzania danych, omówimy, jak główne silniki przeglądarek obsługują ewaluację skryptów, ponieważ ich zachowanie w tym zakresie jest różne.

Skrypty wczytane za pomocą elementu <script>

Liczba zadań wysyłanych do oceny skryptów jest zwykle wprost proporcjonalna do liczby elementów <script> na stronie. Każdy element <script> uruchamia zadanie polegające na ocenie żą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 do zarządzania skryptami produkcyjnymi używasz narzędzia do łączenia i maszowego przetwarzania, które skonfigurowano tak, aby łączyć wszystko, czego potrzebuje Twoja strona, w jeden skrypt. Jeśli tak jest w przypadku Twojej witryny, możesz oczekiwać, że zostanie wysłane jedno zadanie do oceny tego skryptu. Czy to źle? Niekoniecznie – chyba że skrypt jest ogromny.

Możesz podzielić proces oceny skryptu, unikając wczytywania dużych fragmentów kodu JavaScript, i wczytywać więcej mniejszych skryptów za pomocą dodatkowych elementów <script>.

Chociaż zawsze należy dążyć do wczytania jak najmniej kodu JavaScriptu podczas wczytywania strony, dzielenie skryptów na mniejsze części sprawia, że zamiast jednego dużego zadania, które może blokować główny wątek, masz większą liczbę mniejszych zadań, które w ogóle nie będą blokować głównego wątku lub przynajmniej będą blokować go w mniejszym stopniu.

Wiele zadań polegających na ocenie skryptu, które są wizualizowane w profilu wydajności w Narzędziech deweloperskich w Chrome. Ponieważ wczytywane jest wiele mniejszych skryptów zamiast kilku większych, zadania rzadziej stają się długie, co pozwala głównemu wątkowi szybciej reagować na dane wejściowe użytkownika.
W wyniku obecności wielu elementów <script> w kodzie HTML strony powstało wiele zadań służących do oceny skryptów. Jest to lepsze rozwiązanie niż wysyłanie do użytkowników jednego dużego pakietu skryptów, który może zablokować główny wątek.

Dzielenie zadań na potrzeby oceny skryptu można traktować jako coś podobnego do zwracania wartości w wywołaniach zwrotnych zdarzeń wykonywanych podczas interakcji. Jednak w przypadku oceny skryptu mechanizm yielding dzieli wczytywany przez Ciebie kod JavaScript na wiele mniejszych skryptów, a nie na mniejszą liczbę większych skryptów, które łatwiej zablokują główny wątek.

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

Teraz można wczytywać moduły ES natywnie w przeglądarce za pomocą atrybutu type=module w elemencie <script>. Takie podejście do wczytywania skryptów niesie ze sobą pewne korzyści dla programistów, np. brak konieczności przekształcania kodu na potrzeby wersji produkcyjnej, 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 ich pochodnych wczytywanie modułów ES za pomocą atrybutu type=module powoduje wykonywanie innych zadań niż w przypadku, gdy nie używasz atrybutu type=module. Na przykład w przypadku każdego skryptu modułu zostanie uruchomione zadanie, które obejmuje aktywność oznaczoną jako Kompilowanie modułu.

Kompilacja modułu w wielu zadaniach, jak widać w narzędziach deweloperskich w Chrome.
Zachowanie ładowania modułów w przeglądarkach opartych na Chromium. Każdy skrypt modułu wywoła wywołanie Zkompiluj moduł, aby skompilować jego zawartość przed oceną.

Po skompilowaniu modułów każdy kod, który następnie w nich działa, uruchamia aktywność o nazwie Evaluate module (Ocenianie modułu).

Weryfikacja na bieżąco modułu, jaką można zobaczyć w panelu wydajności w Narzędziach deweloperskich w Chrome.
Gdy kod w module jest uruchamiany, moduł jest oceniany w czasie rzeczywistym.

W przypadku Chrome i powiązanych przeglądarek oznacza to, że podczas korzystania z modułów ES kroki kompilacji są podzielone. Jest to wyraźna korzyść w zakresie zarządzania długimi zadaniami, ale wynikająca z tego ocena modułu oznacza, że nadal ponosisz pewne nieuniknione koszty. Chociaż należy dążyć do tego, aby przesyłać jak najmniej kodu JavaScript, używanie modułów ES (niezależnie od przeglądarki) niesie ze sobą następujące korzyści:

  • Cały kod modułu jest automatycznie uruchamiany w trybie rygorystycznym, co umożliwia potencjalne optymalizacje przez silniki JavaScript, których nie można byłoby w innym przypadku wykonać w kontekście nierygorystycznym.
  • Skrypty wczytane za pomocą type=module są traktowane tak, jakby były domyślnie opóźnione. Aby zmienić to zachowanie, możesz użyć atrybutu async w skryptach wczytywanych za pomocą type=module.

Safari i Firefox

Podczas wczytywania modułów w Safari i Firefox każdy z nich jest oceniany w ramach osobnego zadania. Oznacza to, że teoretycznie można wczytać pojedynczy moduł najwyższego poziomu, który składa się tylko z wystąpień static import w innych modułach. Każdy wczytywany moduł spowoduje wysłanie osobnego żądania sieci i osobnego zadania do jego oceny.

Skrypty wczytane za pomocą dynamicznego import()

Dynamiczne import() to inna metoda wczytywania skryptów. W odróżnieniu od statycznych instrukcji import, które muszą znajdować się na początku modułu ES, wywołanie dynamiczne import() może pojawić się w dowolnym miejscu w kodzie, aby wczytać fragment kodu JavaScript na żądanie. Ta technika nazywa się dzieleniem kodu.

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

  1. Moduły, które są odroczone do późniejszego wczytania, zmniejszają współzawodnictwo głównego wątku podczas uruchamiania, ponieważ zmniejszają ilość wczytywanego w tym czasie kodu JavaScript. Dzięki temu główny wątek może szybciej reagować na interakcje użytkowników.
  2. Gdy wykonywane są dynamiczne wywołania import(), każde wywołanie skutecznie oddziela kompilację i ocenę każdego modułu do własnego zadania. Oczywiście dynamiczna funkcja import(), która wczytuje bardzo duży moduł, spowoduje uruchomienie dość dużego zadania oceny skryptu, co może zakłócić zdolność głównego wątku do reagowania na dane wejściowe użytkownika, jeśli interakcja występuje w tym samym czasie co wywołanie dynamicznej funkcji import(). Dlatego ważne jest, aby wczytywać jak najmniej kodu JavaScript.

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

Skrypty wczytywane w procesie web worker

Workery internetowi to specjalny przypadek użycia JavaScriptu. Wątki web worker są rejestrowane w wątku głównym, a kod w tym wątku jest wykonywany w osobnym wątku. Jest to bardzo korzystne, ponieważ kod rejestrujący web workera działa na głównym wątku, ale kod wewnątrz web workera nie. Dzięki temu zmniejsza się obciążenie głównego wątku, co może poprawić jego reakcję na interakcje użytkowników.

Oprócz zmniejszania obciążenia głównego wątku instancje robocze same mogą wczytywać skrypty zewnętrzne do użycia w kontekście instancji roboczej za pomocą instrukcji importScripts lub statycznych instrukcji import w przeglądarkach, które obsługują moduły instancji roboczej. W efekcie każdy skrypt wywołany przez instancję roboczą jest oceniany poza wątkiem głównym.

Uwzględniane kompromisy i rozważania

Dzielenie skryptów na oddzielne, mniejsze pliki pomaga ograniczyć czas wykonywania długich zadań, które wymagają wczytania mniejszej liczby większych plików. Decydując się na podział skryptów, należy jednak wziąć pod uwagę kilka kwestii.

Wydajność kompresji

Kompresja to czynnik, który ma znaczenie w przypadku dzielenia skryptów. Gdy skrypty są mniejsze, kompresja staje się nieco mniej wydajna. Kompresja przyniesie znacznie większe korzyści w przypadku większych skryptów. Zwiększenie skuteczności kompresji pomaga skrócić czas wczytywania skryptów, ale trzeba przy tym zachować równowagę, aby podzielić skrypty na wystarczająco małe fragmenty, które ułatwią interakcję podczas uruchamiania.

Pakiety są idealnym narzędziem do zarządzania rozmiarem danych wyjściowych skryptów, od których zależy Twoja witryna:

  • Jeśli chodzi o webpack, może Ci pomóc wtyczka SplitChunksPlugin. Więcej informacji o opcjach, które możesz ustawić, aby zarządzać rozmiarami komponentów, znajdziesz w dokumentacji SplitChunksPlugin.
  • W przypadku innych narzędzi do łączenia, takich jak Rollupesbuild, możesz zarządzać rozmiarami plików skryptów, używając w kodzie dynamicznych wywołań import(). Te programy do tworzenia pakietów, a także webpack, automatycznie rozdzielą komponent importowany dynamicznie do własnego pliku, dzięki czemu unikniesz większych rozmiarów początkowych pakietów.

Unieważnienie pamięci podręcznej

Unieważnia pamięci podręcznej ma duży wpływ na szybkość wczytywania strony podczas ponownych wizyt. Jeśli wysyłasz duże, monolityczne pakiety skryptów, nie masz przewagi, jeśli chodzi o zapamiętywanie w pamięci podręcznej przeglądarki. Dzieje się tak, ponieważ gdy aktualizujesz kod własnego źródła (poprzez aktualizację pakietów lub wysyłkę poprawek błędów), cały pakiet staje się nieważny i musi zostać ponownie pobrany.

Podzielając skrypty, nie tylko dzielisz pracę związaną z ich analizą na mniejsze zadania, ale też zwiększasz prawdopodobieństwo, że powracający użytkownicy będą pobierać więcej skryptów z pamięci podręcznej przeglądarki zamiast z sieci. Oznacza to ogólnie szybsze wczytywanie stron.

Moduły zagnieżdżone i wydajność wczytywania

Jeśli wysyłasz moduły ES w wersji produkcyjnej i ładujesz je za pomocą atrybutu type=module, musisz wiedzieć, jak zagnieżdżanie modułów może wpływać na czas uruchamiania. Zagnieżdżenie modułów występuje, gdy moduł ES importuje statycznie inny moduł ES, który importuje statycznie jeszcze inny moduł ES:

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

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

Jeśli moduły ES nie są zgrupowane, powyższy kod powoduje łańcuch żądań sieciowych: gdy element <script> wysyła żądanie a.js, wysyłane jest kolejne żądanie sieciowe dotyczące elementu b.js, które z kolei powoduje kolejne żądanie dotyczące elementu c.js. Jednym ze sposobów uniknięcia tego problemu jest użycie narzędzia do łączenia, ale pamiętaj, aby skonfigurować je tak, aby dzieliło skrypty na części, co pozwoli rozłożyć w czasie proces ich oceny.

Jeśli nie chcesz używać pakietu, możesz zastosować inny sposób obejścia zagnieżdżonych wywołań modułów, korzystając z modulepreloadwskazówki zasobu, która wczyta z wyprzedzeniem moduły ES, aby uniknąć łańcuchów żądań sieciowych.

Podsumowanie

Optymalizacja oceny skryptów w przeglądarce jest bez wątpienia trudnym zadaniem. Wybór metody zależy od wymagań i ograniczeń Twojej witryny. Jednak dzieląc skrypty, rozpraszasz pracę związaną z ich wykonywaniem na wiele mniejszych zadań, co pozwala wątkowi głównemu efektywniej obsługiwać interakcje z użytkownikiem, a nie blokować go.

Oto kilka sposobów na podzielenie dużych zadań weryfikacji skryptu:

  • Podczas wczytywania skryptów za pomocą elementu <script> bez atrybutu type=module unikaj wczytywania bardzo dużych skryptów, ponieważ uruchamiają one wymagające wielu zasobów zadania oceny skryptu, które blokują wątek główny. Rozłóż skrypty na więcej elementów <script>, aby podzielić pracę.
  • Użycie atrybutu type=module do wczytania modułów ES natywnie w przeglądarce spowoduje rozpoczęcie poszczególnych zadań do oceny każdego osobnego skryptu modułu.
  • Zmniejsz rozmiar początkowych pakietów, używając dynamicznych wywołań import(). Funkcja ta działa również w programach do łączenia, ponieważ traktowane są one jako „punkty podziału” i generują osobny skrypt dla każdego importowanego dynamicznie modułu.
  • Pamiętaj, aby wziąć pod uwagę kompromisy, takie jak wydajność kompresji i unieważnia pamięci podręcznej. Większe skrypty są lepiej kompresowane, ale prawdopodobnie wymagają bardziej kosztownego przetwarzania w ramach mniejszej liczby zadań i spowodują unieważnienie pamięci podręcznej przeglądarki, co prowadzi do ogólnie niższej wydajności pamięci podręcznej.
  • Jeśli używasz modułów ES natywnie bez łączenia, użyj podpowiedzi zasobów modulepreload, aby zoptymalizować ich wczytywanie podczas uruchamiania.
  • Jak zawsze, wysyłaj jak najmniej kodu JavaScript.

To z pewnością sztuka wyważenia, ale dzieląc skrypty i zmniejszając początkowe ładunki za pomocą dynamicznego import(), możesz uzyskać lepszą wydajność uruchamiania i lepiej dostosować interakcje z użytkownikiem w tym kluczowym okresie uruchamiania. Dzięki temu możesz uzyskać wyższą wartość wskaźnika INP, a tym samym zapewnić lepsze wrażenia użytkownikom.