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) większość porad dotyczy optymalizacji samych interakcji. Na przykład w przewodniku Optymalizacja długich zadań omawiane są techniki takie 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.

Co jednak 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 działania 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ę Ocenianie 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.
Ocena skryptu działa w sposób pokazany w narzędziu do profilowania 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. Oceniany skrypt najpierw jest 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.

Ocena skryptu może być problematyczna, choć użytkownicy mogą próbować wejść w interakcję ze stroną zaraz po jej wyrenderowaniu. Jednak to, że strona została wyrenderowana, nie oznacza, że została już w pełni wczytana. Interakcje zachodzące podczas wczytywania mogą być opóźnione, ponieważ strona jest wtedy analizowana przez skrypty. Chociaż nie ma gwarancji, że interakcja może nastąpić w tym momencie – ponieważ odpowiedzialny za nią skrypt może się jeszcze nie wczytać – mogą istnieć interakcje zależne od kodu JavaScript, które są gotowe, lub interakcja w ogóle 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 ładowany skrypt jest wczytywany przez typowy element <script>, czy jest to moduł ładowany za pomocą metody 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 w celu oceny skryptów zwykle jest bezpośrednio związana z liczbą 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.

Aby podzielić zadania oceny skryptu, unikaj wczytywania dużych fragmentów kodu JavaScript, i wczytuj bardziej pojedyncze, mniejsze skrypty, korzystając z dodatkowych elementów <script>.

Staraj się zawsze starać się wczytywać jak najmniej JavaScriptu podczas wczytywania strony, jednak podzielenie skryptów sprawia, że zamiast jednego dużego zadania, które mogą blokować wątek główny, masz więcej mniejszych zadań, które w ogóle nie zablokują głównego wątku, a przynajmniej mniej niż te na początku.

Wiele zadań polegających na ocenie skryptu, które są wizualizowane w profilu wydajności w Narzędziech deweloperskich Chrome. Ponieważ zamiast kilku większych skryptów wczytywane są liczne mniejsze skrypty, 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 z atrybutem 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 powoduje wywołanie wywołania compilacja modułu, 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 efekcie podczas korzystania z modułów ES kroki kompilacji są podzielone (przynajmniej w Chrome i powiązanych przeglądarkach). Jest to wyraźna korzyść w zakresie zarządzania długimi zadaniami, ale wynikająca z tego ocena modułu wiąże się z nieuniknionymi kosztami. 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ą domyślnie traktowane tak, jakby były odrywane. Aby zmienić to zachowanie, możesz użyć atrybutu async w skryptach wczytywanych za pomocą type=module.

Safari i Firefox

Gdy moduły są wczytywane w przeglądarkach Safari i Firefox, każdy z nich jest oceniany w osobnym zadaniu. 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 się pojawić w dowolnym miejscu w kodzie, aby wczytać fragment kodu JavaScript na żądanie. Ta metoda jest nazywana podziałem kodu.

Dynamiczna import() ma 2 zalety związane z poprawą wartości INP:

  1. Moduły, które są odroczone do późniejszego wczytania, zmniejszają obciążenie głównego wątku podczas uruchamiania, ponieważ zmniejszają ilość wczytywanego w tym czasie kodu JavaScript. Pozwoli to zwolnić wątek główny, aby można było łatwiej reagować na interakcje użytkowników.
  2. W przypadku dynamicznych wywołań funkcji import() każde wywołanie powoduje rozdzielenie kompilacji i oceny każdego modułu do osobnego zadania. Oczywiście dynamiczna funkcja import(), która wczytuje bardzo duży moduł, spowoduje rozpoczęcie 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 nastąpi 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 wczytane do instancji roboczej

Praca wątek w przeglądarce to szczególny przypadek użycia JavaScriptu. Instancje robocze są rejestrowane w wątku głównym, a ich kod jest następnie uruchamiany we własnym 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.

Kompromisy i kwestie

Podzielenie skryptów na osobne pliki o mniejszych plikach pomaga ograniczyć liczbę długich zadań zamiast wczytywania mniejszej liczby znacznie większych plików. Jednak przy podejmowaniu decyzji o sposobie podziału skryptów należy 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 będzie dużo bardziej przydatna dla większych skryptów. Zwiększenie wydajności kompresji pomaga zminimalizować czas wczytywania skryptów, ale wymaga to zachowania równowagi, która polega na tym, żeby podzielić skrypty na wystarczającą liczbę mniejszych fragmentów, co usprawni interaktywność 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. Informacje o opcjach ułatwiających zarządzanie rozmiarami zasobów znajdziesz w dokumentacji usługi SplitChunksPlugin.
  • W przypadku innych narzędzi do łączenia, takich jak Rollupesbuild, możesz zarządzać rozmiarami plików skryptu, używając w kodzie dynamicznych wywołań import(). Te programy do tworzenia pakietów, a także webpack, automatycznie rozdzielą dynamicznie importowany zasób na własny plik, dzięki czemu unikniesz większych rozmiarów początkowych pakietów.

Unieważnienie pamięci podręcznej

Unieważnianie 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 własny kod (poprzez aktualizację pakietów lub wysyłkę poprawek błędów), cały pakiet staje się nieważny i musi zostać ponownie pobrany.

Dzieląc skrypty, nie tylko dzielisz zadanie oceny skryptu na mniejsze zadania, ale zwiększasz też prawdopodobieństwo, że powracający użytkownicy pobiorą więcej skryptów z pamięci podręcznej przeglądarki, a nie 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żanie modułów polega na tym, że 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ą połączone w pakiet, poprzedni kod tworzy łańcuch żądań sieciowych. Gdy z elementu <script> otrzymuje żądanie a.js, dla b.js wysyłane jest kolejne żądanie sieciowe, które wiąże się z kolejnym żądaniem dotyczącym 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 korzystać z narzędzia do tworzenia pakietów, innym sposobem na obejście wywołań modułów zagnieżdżonych jest skorzystanie ze wskazówki dotyczącej zasobów modulepreload, która powoduje wstępne załadowanie modułów ES z wyprzedzeniem, 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 rzeczy, które możesz zrobić, aby rozdzielić duże zadania oceny 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ć tę 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 też w programach do tworzenia pakietów, 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 kompresują się lepiej, ale z większym prawdopodobieństwem wymagają droższej oceny skryptów przy mniejszej liczbie zadań. Powoduje to unieważnienie pamięci podręcznej przeglądarki, co prowadzi do zmniejszenia wydajności buforowania.
  • Jeśli używasz natywnie modułów ES bez grupowania, skorzystaj ze wskazówki dotyczącej zasobów modulepreload, aby zoptymalizować ich ładowanie podczas uruchamiania.
  • Jak zawsze przesyłaj jak najmniej JavaScriptu.

To z pewnością sztuka równowagi, ale dzieląc skrypty i skracając początkowe ładunki za pomocą dynamicznego import(), możesz uzyskać lepszą wydajność podczas uruchamiania i lepiej dopasować 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.