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 jak możesz zapobiec wywoływaniu przez nią długich zadań podczas wczytywania strony.

Jeśli chodzi o optymalizowanie interakcji do następnego wyrenderowania (INP), większość porad dotyczących samodzielnego optymalizowania interakcji. Na przykład w przewodniku po optymalizacji długich zadań omawiamy techniki takie jak osiąganie za pomocą setTimeout, isInputPending itd. Techniki te są przydatne, ponieważ dają wątkowi nieco więcej miejsca, ponieważ unikają długich zadań, co może zwiększyć szanse na szybsze rozpoczęcie interakcji i innych działań, a nie w przypadku, gdy trzeba będzie czekać na jedno długie zadanie.

Co jednak z długimi zadaniami wynikającymi z wczytywania samych skryptów? Te zadania mogą zakłócać interakcje użytkowników i wpływać na wartość INP strony podczas jej wczytywania. W tym przewodniku omawiamy, jak przeglądarki obsługują zadania uruchamiane przez ocenę skryptu, i pokazujemy, co możesz zrobić, aby rozdzielić zadania oceny skryptu, tak aby wątek główny mógł lepiej reagować na dane wprowadzane przez użytkownika podczas wczytywania strony.

Co to jest ocena skryptu?

Jeśli profilujesz aplikację, która wysyła dużo kodu JavaScript, być może zauważysz długie zadania z etykietą Oceń skrypt.

Ocena skryptów działa w sposób przedstawiony w programie profilu wydajności Narzędzi deweloperskich w Chrome. Praca podczas uruchamiania powoduje długie zadanie, co uniemożliwia reagowanie wątku głównego na interakcje użytkowników.
Ocena skryptów działa tak, jak to pokazuje w programie profilu wydajności w Narzędziach deweloperskich w Chrome. W takim przypadku taka praca wystarczy, aby wykonać długie zadanie, które uniemożliwia wątekowi głównemu wykonywanie innych zadań, w tym zadań generujących interakcje użytkowników.

Ocena skryptu jest niezbędna podczas wykonywania JavaScriptu w przeglądarce, ponieważ JavaScript jest kompilowany w odpowiednim momencie przed wykonaniem. Podczas sprawdzania skryptu jest on najpierw analizowany pod kątem błędów. Jeśli parser nie znajdzie błędów, skrypt zostanie skompilowany do postaci bytecode i może kontynuować jego wykonywanie.

Mimo że ocena skryptu jest konieczna, może to powodować problemy, ponieważ użytkownicy mogą próbować wejść w interakcję ze stroną wkrótce po jej pierwszym wyrenderowaniu. Wyrenderowanie strony nie oznacza jednak, że zakończyło się wczytywanie. Interakcje mające miejsce podczas wczytywania mogą być opóźnione, ponieważ strona jest zajęta ocenianiem skryptów. Chociaż nie możemy zagwarantować, że oczekiwana interakcja zajdzie w danym momencie (ponieważ odpowiedzialny za nie skrypt jeszcze się nie wczytał), możliwe, że interakcje zależne od JavaScriptu gotowe lub interaktywność nie zależy w ogóle 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 przez zwykły element <script>, czy też skrypt jest modułem ładowanym przez interfejs type=module. Przeglądarki często obsługują różne działania, dlatego sposób, w jaki główne wyszukiwarki obsługują ocenę skryptów, będzie miał wpływ na różnice w działaniach związanych z oceną skryptów.

Wczytuję skrypty z elementem <script>

Liczba zadań wysłanych w celu oceny skryptów zwykle jest bezpośrednio powiązana z liczbą elementów <script> na stronie. Każdy element <script> uruchamia zadanie w celu oceny żądanego skryptu oraz jego analizy, skompilowania i wykonania. Dotyczy to przeglądarek opartych na Chromium oraz Safari oraz Firefox.

Dlaczego ma to znaczenie? Załóżmy, że używasz narzędzia do zarządzania skryptami produkcyjnymi i masz go tak skonfigurowany, by zebrać w jednym skrypcie wszystko, czego potrzebuje Twoja strona. Jeśli tak jest w przypadku Twojej witryny, zostanie wysłane pojedyncze zadanie służące do oceny danego skryptu. Czy to źle? Niekoniecznie, chyba że skrypt jest duży.

Możesz podzielić pracę na etapie oceny skryptów, unikając ładowania dużych fragmentów JavaScriptu, i wczytywać więcej pojedynczych, mniejszych skryptów przy użyciu dodatkowych elementów <script>.

Staraj się zawsze ładować jak najmniej kodu JavaScript podczas wczytywania strony, ale podzielenie skryptów na serwer spowoduje, że zamiast jednego dużego zadania, które może zablokować wątek główny, będziesz mieć większą liczbę mniejszych zadań, które w ogóle nie zablokują głównego wątku, albo przynajmniej mniej niż to, od którego zaczęto.

Różne zadania obejmujące ocenę skryptu, co widać w programie profilu wydajności Narzędzi deweloperskich w Chrome. Ponieważ wczytywana jest większa liczba mniejszych skryptów zamiast mniejszej ich liczby, zadania z mniejszym prawdopodobieństwem staną się długimi zadaniami. Dzięki temu wątek główny będzie szybciej reagować na działania użytkownika.
W celu oceny skryptów na skutek obecności wielu elementów <script> w kodzie HTML strony powstało wiele zadań. Najlepiej jest wysłać użytkownikom 1 duży pakiet skryptów, ponieważ zwiększa to ryzyko zablokowania głównego wątku.

Podział zadań na potrzeby oceny skryptu przypomina zysk w trakcie wywołań zwrotnych zdarzeń wykonywanych w ramach interakcji. Jednak w przypadku oceny skryptów mechanizm zysku dzieli ładowany JavaScript na kilka mniejszych skryptów, a nie mniejszą liczbę większych skryptów, ponieważ istnieje większe prawdopodobieństwo, że zablokuje on wątek główny.

Wczytuję skrypty z elementem <script> i atrybutem type=module

Moduły ES można teraz ładować natywnie w przeglądarce za pomocą atrybutu type=module w elemencie <script>. To podejście do wczytywania skryptów ma pewne zalety dla programistów, takie jak brak konieczności przekształcania kodu do użytku w środowisku produkcyjnym – szczególnie w połączeniu z mapami importu. Jednak wczytywanie skryptów w ten sposób umożliwia 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 Chrome – wczytanie modułów ES za pomocą atrybutu type=module powoduje utworzenie innych zadań niż zwykle, gdy nie używasz type=module. Na przykład w przypadku każdego skryptu modułu zostanie uruchomione zadanie z działaniem oznaczonym jako Kompilowanie modułu.

Kompilacja modułów działa w ramach wielu zadań, co widać w Narzędziach deweloperskich w Chrome.
Sposób ładowania modułu w przeglądarkach opartych na Chromium. Każdy skrypt modułu generuje wywołanie kompilacji modułu, które skompilowa jego zawartość przed oceną.

Po skompilowaniu każdego kodu, który zostanie w nich uruchomiony, rozpocznie się działanie oznaczone jako Oceń moduł.

Ocena modułu w czasie pokazana w panelu wydajności Narzędzi deweloperskich w Chrome.
Gdy kod w module zostanie uruchomiony, zostanie on oceniony w odpowiednim czasie.

W efekcie w przypadku Chrome i podobnych przeglądarek internetowych proces kompilacji jest teraz podzielony w przypadku używania modułów ES. Jest to wyraźna korzyść w zakresie zarządzania długimi zadaniami, ale wynik oceny modułu nadal niesie ze sobą pewne nieuniknione koszty. Staraj się przesyłać jak najmniejszą ilość kodu JavaScriptu, jednak stosowanie modułów ES, niezależnie od używanej przeglądarki, ma następujące korzyści:

  • Cały kod modułu jest automatycznie uruchamiany w trybie ścisłym, co umożliwia silnikom JavaScriptu potencjalne optymalizacje, których nie dałoby się przeprowadzić w nieścisłym kontekście.
  • Skrypty załadowane przy użyciu type=module są domyślnie odroczone. Możesz zmienić to działanie, korzystając z atrybutu async w skryptach wczytywanych za pomocą type=module.

Safari i Firefox

Gdy moduły są ładowane w Safari i Firefoksie, każdy z nich jest oceniany w ramach osobnego zadania. Oznacza to, że teoretycznie możesz wczytać do innych modułów jeden moduł najwyższego poziomu zawierający tylko statyczne instrukcje import, a każdy załadowany moduł wiąże się z osobnym żądaniem sieciowym i osobnym zadaniem do jego oceny.

Wczytuję skrypty za pomocą dynamicznego import()

Dynamiczny import() to inna metoda wczytywania skryptów. W odróżnieniu od statycznych instrukcji import, które muszą się znajdować na górze modułu ES, dynamiczne wywołanie import() może pojawić się w dowolnym miejscu skryptu w celu wczytania fragmentu kodu JavaScript na żądanie. Ta technika to podział kodu.

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

  1. Moduły, które zostały odłożone na później do wczytania, ograniczają rywalizację z wątkami głównymi podczas uruchamiania, zmniejszając ilość kodu JavaScript wczytywanego w tym czasie. Wątek główny uwolni się i będzie mógł lepiej odpowiadać na interakcje użytkowników.
  2. Gdy wykonywane są dynamiczne wywołania import(), każde wywołanie skutecznie rozdzieli kompilację i ocenę każdego modułu na osobne zadanie. Oczywiście dynamiczny import(), który wczytuje bardzo duży moduł, uruchamia dość duże zadanie oceny skryptu, co może zakłócać reagowanie wątku głównego na dane wejściowe użytkownika, jeśli interakcja zajdzie w tym samym czasie co dynamiczne wywołanie import(). Dlatego nadal bardzo ważne jest wczytywanie jak najmniejszej ilości kodu JavaScript.

Dynamiczne wywołania import() działają podobnie we wszystkich głównych wyszukiwarkach – wykonywane zadania oceny skryptów będą takie same jak liczba modułów, które są dynamicznie importowane.

Wczytuję skrypty w narzędziu Web worker

Roboty internetowe to specjalne przypadki użycia JavaScriptu. Instancje robocze są rejestrowane w wątku głównym, a kod w tej instancji roboczej uruchamia się w jednym wątku. Jest to niezwykle przydatne w tym sensie, że chociaż kod rejestrujący instancję roboczą działa w wątku głównym, kod w narzędziu internetowym nie działa. Zmniejsza to obciążenie głównego wątku i ułatwia jego reagowanie na interakcje użytkowników.

Oprócz ograniczenia pracy w wątku głównym, roboty internetowe sami mogą wczytywać skrypty zewnętrzne, które będą używane w kontekście instancji roboczych za pomocą instrukcji importScripts lub statycznych import w przeglądarkach, które obsługują zasoby robocze modułu. W efekcie każdy skrypt żądany przez instancję roboczą jest oceniany poza wątkiem głównym.

Kompromisy i kwestie

Podział skryptów na osobne, mniejsze pliki pomaga ograniczyć czasochłonne zadania, a nie ładować mniej, ale znacznie większych plików. Przy podejmowaniu decyzji, w jaki sposób podzielić skrypty należy wziąć pod uwagę pewne kwestie.

Wydajność kompresji

Kompresja jest jednym z czynników przy dzieleniu skryptów. Gdy skrypty są mniejsze, kompresja staje się nieco mniej efektywna. Większe skrypty będą miały znacznie większy wpływ na kompresję. Zwiększenie wydajności kompresji pomaga w utrzymaniu jak najkrótszych czasów wczytywania skryptów, ale w trosce o równowagę pozwala uzyskać większe odstępy między skryptami i ułatwi lepszą interaktywność podczas uruchamiania.

Pakiety to idealne narzędzia do zarządzania rozmiarem danych wyjściowych skryptów w witrynie, na których podstawie:

  • W razie problemów z pakietem internetowym może pomóc jego wtyczka SplitChunksPlugin. Informacje o opcjach, które ułatwią Ci zarządzanie rozmiarami zasobów, znajdziesz w dokumentacji usługi SplitChunksPlugin.
  • W przypadku innych pakietów, takich jak Rollup i esbuild, możesz zarządzać rozmiarami plików skryptów, używając dynamicznych wywołań import() w kodzie. Te pakiety, podobnie jak pakiet internetowy, automatycznie dzielą dynamicznie importowany zasób na osobny plik, co pozwala uniknąć większych początkowych rozmiarów pakietu.

Unieważnienie pamięci podręcznej

Unieważnienie pamięci podręcznej ma duże znaczenie dla szybkości wczytywania strony przy kolejnych wizytach. Jeśli wysyłasz duże pakiety skryptów monolitycznych, nic Cię to nie ogranicza, jeśli chodzi o pamięć podręczną przeglądarki. Dzieje się tak, ponieważ gdy zaktualizujesz swój kod (np. aktualizując paczki lub poprawki błędów dostawy), cały pakiet staje się unieważniony i trzeba go ponownie pobrać.

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

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

Jeśli przesyłasz moduły ES w wersji produkcyjnej i wczytujesz je za pomocą atrybutu type=module, musisz wiedzieć, jak zagnieżdżenie modułów może wpłynąć na czas uruchamiania. Zagnieżdżanie modułu oznacza statycznie import innego modułu ES, który statycznie importuje inny moduł ES:

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

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

Jeśli moduły ES nie są ze sobą połączone, poprzedni kod tworzy łańcuch żądań sieciowych: gdy żądanie a.js pochodzi z elementu <script>, wysyłane jest inne żądanie sieciowe dla b.js, które następnie wiąże się z kolejnym żądaniem dotyczącym c.js. Jeśli chcesz tego uniknąć, możesz użyć usługi tworzenia pakietów. Pamiętaj jednak, aby skonfigurować ją tak, aby dzielić skrypty w celu rozdzielenia zadań związanych z oceną skryptów.

Jeśli nie chcesz korzystać z pakietu, innym sposobem na ominięcie zagnieżdżonych wywołań modułów jest skorzystanie ze wskazówki dotyczącej zasobów modulepreload, która wstępnie wczytuje moduły ES z wyprzedzeniem, aby uniknąć łańcuchów żądań sieciowych.

Podsumowanie

Optymalizacja oceny skryptów w przeglądarce nie jest bez wątpienia trudna. To podejście zależy od wymagań i ograniczeń Twojej witryny. Jednak rozdzielając skrypty na wiele mniejszych zadań, dasz wątkowi głównemu możliwość wydajniejszej obsługi interakcji użytkowników, zamiast blokować wątek główny.

Oto kilka czynności, które możesz wykonać, aby podzielić duże zadania oceny skryptów:

  • Podczas wczytywania skryptów przy użyciu elementu <script> bez atrybutu type=module unikaj wczytywania bardzo dużych skryptów, ponieważ uruchamiają one pracochłonne zadania oceny skryptów, które blokują wątek główny. Aby to ułatwić, rozmieść skrypty na większej liczbie elementów <script>.
  • Użycie atrybutu type=module do natywnego wczytania modułów ES w przeglądarce spowoduje uruchomienie poszczególnych zadań do oceny dla każdego skryptu z osobnego modułu.
  • Zmniejsz rozmiar początkowych pakietów, używając dynamicznych wywołań import(). Jest to również możliwe w przypadku pakietów, które będą traktować każdy dynamicznie importowany moduł jako „punkt podziału”, co spowoduje wygenerowanie osobnego skryptu dla każdego dynamicznie zaimportowanego modułu.
  • Weź pod uwagę takie korzyści jak wydajność kompresji i unieważnienie pamięci podręcznej. Większe skrypty są lepiej kompresowane, ale częściej wymagają droższej oceny skryptów w mniejszej liczbie zadań, co prowadzi do unieważnienia 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 grupowania, skorzystaj ze wskazówki dotyczącej zasobów modulepreload, aby zoptymalizować ich ładowanie podczas uruchamiania.
  • Jak zawsze, staraj się przesyłać jak najmniej JavaScriptu.

To sposób na osiągnięcie równowagi. Jednak rozbicie skryptów i zmniejszenie ładunków początkowych z wykorzystaniem dynamicznego import() pozwoli Ci uzyskać większą wydajność uruchamiania i lepiej dostosować się do interakcji użytkowników w tym kluczowym okresie uruchamiania. Powinno to pomóc Ci uzyskać lepszy wynik w danych INP, a tym samym zapewnić użytkownikom lepsze wrażenia.

Baner powitalny z serialu Unsplash, autor: Markus Spiske.