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 dotyczącym optymalizacji długich zadań omawiane są takie techniki jak setTimeout
. Te techniki są korzystne, ponieważ dają głównemu wątkowi trochę przestrzeni na unikanie długich zadań, co może zwiększyć liczbę interakcji i innych działań, które mogą być wykonywane wcześniej, niż gdyby musiały czekać 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 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ę Evaluate Script (Sprawdzanie skryptu).
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 są 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 też 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.
Dzielenie zadań na potrzeby oceny skryptu można traktować jako coś podobnego do zwracania wartości podczas wywołań zwrotnych zdarzeń, które są wykonywane 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.
Po skompilowaniu modułów każdy kod, który następnie w nich działa, uruchamia aktywność o nazwie Evaluate module (Ocenianie modułu).
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ą domyślnie traktowane tak, jakby były odrywane. Aby zmienić to zachowanie, możesz użyć atrybutuasync
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 kolejna 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:
- 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. Dzięki temu główny wątek może szybciej reagować na interakcje użytkowników.
- Gdy wywołania dynamiczne
import()
są wykonywane, każde wywołanie skutecznie oddziela kompilację i ocenę każdego modułu do własnego zadania. Oczywiście dynamiczna funkcjaimport()
, 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 funkcjiimport()
. 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
Praca wątek w przeglądarce to szczególny przypadek użycia JavaScriptu. Wątki web worker są rejestrowane w wątku głównym, a kod w tym wątku jest wykonywany w ramach własnego wątku. Jest to bardzo korzystne, ponieważ kod rejestrujący web workera działa na głównym wątku, ale sam web worker 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, weź pod uwagę kilka kwestii.
Wydajność kompresji
Kompresja jest czynnikiem, 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 dokumentacjiSplitChunksPlugin
. - W przypadku innych narzędzi do łączenia, takich jak Rollup i esbuild, 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ą 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.
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żanie modułów polega na tym, że moduł ES importuje statycznie inny moduł ES, który z kolei 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 modulepreload
wskazó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 zadanie oceny skryptu 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 atrybututype=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 równowagi, ale dzieląc skrypty i skracając początkowe ładunki za pomocą dynamicznego import()
, możesz uzyskać lepszą wydajność uruchamiania i lepiej dopasować interakcje użytkownika 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.