Ocena skryptu i długie zadania

Ocena skryptów podczas wczytywania skryptów przez przeglądarkę przed wykonaniem może trochę potrwać, co może powodować wykonywanie długich zadań. Dowiedz się, jak działa ocena skryptu i co możesz zrobić, aby nie wydłużać czasu wczytywania strony.

Optymalizacja interakcji do kolejnego wyrenderowania (INP) dotyczy w większości przypadków samodzielnego optymalizowania interakcji. Na przykład w przewodniku po optymalizacji długich zadań omawiamy techniki takie jak uzyskanie zysku z użyciem setTimeout i inne. Te techniki są przydatne, ponieważ dają wątkowi głównemu przestrzeń na odpoczynek dzięki unikaniu długich zadań, co może zapewnić większe możliwości przyspieszenia interakcji i innych aktywności, bez konieczności czekania na pojedyncze długie zadanie.

Co jednak z długimi zadaniami wynikającymi z wczytywania skryptów? Te zadania mogą zakłócać interakcje użytkowników i wpływać na wartość INP strony podczas jej wczytywania. Z tego przewodnika dowiesz się, jak przeglądarki obsługują zadania rozpoczęte przez ocenę skryptu. Podpowiemy też, co możesz zrobić, aby podzielić działanie oceny skryptu, aby wątek główny był bardziej reagowany na działania użytkowników podczas wczytywania strony.

Czym jest ocena skryptu?

Jeśli masz profilowaną aplikację, która wysyła dużo kodu JavaScript, możesz zobaczyć długie zadania z etykietą Oceń skrypt.

Ocena skryptów jest przedstawiona w narzędziu do profilowania wydajności w Narzędziach deweloperskich w Chrome. Ta czynność powoduje długie zadanie podczas uruchamiania, co blokuje zdolność wątku głównego do reagowania 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 taka czynność wystarczy do przeprowadzenia długiego zadania, które zablokuje wątek główny i zablokuje inną pracę, w tym zadania, które zachęcają użytkowników do interakcji.

Ocena skryptu jest niezbędną częścią wykonywania kodu JavaScript w przeglądarce, ponieważ jest kompilowany bezpośrednio przed wykonaniem. Oceniany skrypt najpierw jest analizowany pod kątem błędów. Jeśli parser nie znajdzie błędów, skrypt jest skompilowany do postaci bytecode i może kontynuować wykonywanie.

Ocena skryptu może być problematyczna, choć użytkownicy mogą próbować wejść w interakcję ze stroną zaraz po jej wyrenderowaniu. Wyrenderowanie strony nie oznacza jednak, że została ona 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. Przeglądarki często radzą sobie z różnymi zadaniami, dlatego wspomnimy o tym, jak główne mechanizmy oceny skryptów w nich postępują.

Skrypty wczytane z elementem <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> inicjuje zadanie oceny żądanego skryptu w celu jego przeanalizowania, skompilowania i wykonania. Dotyczy to przeglądarek opartych na Chromium oraz Safari i Firefoksa.

Dlaczego ma to znaczenie? Załóżmy, że do zarządzania skryptami produkcyjnymi używasz narzędzia do tworzenia pakietów i że zostało ono skonfigurowane tak, aby w jednym skrypcie umieszczać wszystko, czego potrzebuje strona. Jeśli tak jest w przypadku Twojej witryny, możesz oczekiwać, że zostanie wysłane jedno zadanie oceny tego skryptu. Czy to źle? Niekoniecznie – chyba że ten skrypt jest wielki.

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.

Kilka zadań z oceną skryptu przedstawione w narzędziu do profilowania wydajności w Narzędziach deweloperskich w Chrome. Wczytywane jest kilka mniejszych skryptów zamiast mniejszych skryptów, przez co zadania rzadziej zmieniają się w długie zadania, dzięki czemu wątek główny może szybciej reagować na dane wejściowe użytkownika.
Pojawiło się kilka zadań służących do oceny skryptów w wyniku działania wielu elementów <script> w kodzie HTML strony. Najlepiej jest wysłać do użytkowników jeden duży pakiet skryptów, ponieważ zwiększa prawdopodobieństwo zablokowania wątku głównego.

Dzielenie zadań na potrzeby oceny skryptu przypomina zyski podczas wywołań zwrotnych zdarzeń, które są uruchamiane w trakcie interakcji. Jednak podczas oceny skryptu mechanizm zyskujący popularność dzieli wczytywany JavaScript na kilka mniejszych skryptów zamiast mniejszej liczby większych skryptów niż prawdopodobieństwo zablokowania głównego wątku.

Skrypty wczytane z elementem <script> i atrybutem 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 skryptu wiąże się z pewnymi korzyściami dla programistów, takimi jak brak konieczności przekształcania kodu do użytku w środowisku produkcyjnym, zwłaszcza w połączeniu z mapami importu. Jednak wczytywanie skryptów w ten sposób planuje zadania, 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 pochodzących z niej narzędziach – ładowanie modułów ES przy użyciu atrybutu type=module powoduje wykonywanie innych rodzajów zadań niż zwykle, gdy nie używasz type=module. Na przykład dla każdego skryptu modułu zostanie uruchomione zadanie obejmujące działanie oznaczone jako Kompilowanie modułu.

Kompilacja modułów działa w wielu zadaniach, co pokazano w Narzędziach deweloperskich w Chrome.
Sposób ł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 zostanie w nich uruchomiony, rozpocznie działanie oznaczone jako Ocena modułu.

Ocena modułu w odpowiednim momencie widoczna w panelu wydajności w Narzędziach deweloperskich w Chrome.
Po uruchomieniu kodu w module zostanie on oceniony dokładnie w chwili.

W efekcie podczas korzystania z modułów ES kroki kompilacji są podzielone (przynajmniej w Chrome i powiązanych przeglądarkach). Zapewnia to oczywiste korzyści w zakresie zarządzania długimi zadaniami. Jednak wynikowa ocena modułów wciąż oznacza, że ponoszone są nieuniknione koszty. Staraj się przesyłać jak najmniej JavaScriptu, jednak stosowanie 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 mechanizmom JavaScript potencjalne optymalizacje, których nie udałoby się przeprowadzić w nieścisłym kontekście.
  • Skrypty wczytywane za pomocą funkcji type=module są domyślnie traktowane tak, jakby były domyślnie odroczone. Aby zmienić to działanie, możesz użyć atrybutu async w przypadku skryptów wczytywanych za pomocą metody 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 by wczytywać do innych modułów pojedynczy moduł najwyższego poziomu zawierający tylko statyczne instrukcje import, a każdy wczytany moduł będzie wymagał osobnego żądania sieciowego do jego oceny.

Skrypty wczytane za pomocą dynamicznego interfejsu import()

Kolejną metodą wczytywania skryptów jest dynamiczna import(). W przeciwieństwie do statycznych instrukcji import, które muszą znajdować się na górze modułu ES, dynamiczne wywołanie import() może pojawić się w dowolnym miejscu skryptu, aby na żądanie wczytać fragment kodu JavaScript. 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ą odłożone w późniejszym czasie, zmniejszają rywalizację o główne wątki podczas uruchamiania, zmniejszając ilość załadowanego wtedy 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 dynamiczny element import(), który wczytuje bardzo duży moduł, uruchomi dość duże zadanie oceny skryptu, co może zakłócać zdolność wątku głównego do reagowania na dane wejściowe użytkownika, jeśli interakcja następuje w tym samym czasie co dynamiczne wywołanie import(). Dlatego ważne jest, by wczytywać jak najmniej JavaScriptu.

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

Skrypty wczytane do instancji roboczej

Instancje robocze to specjalny 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 niezwykle korzystne w tym sensie, że chociaż kod rejestrujący instancję roboczą działa w wątku głównym, to kod w obrębie tego narzędzia – nie. Pozwala to zmniejszyć zagęszczenie wątku głównego i może sprawić, że wątek główny będzie lepiej reagował na interakcje użytkowników.

Poza ograniczaniem pracy wątku głównego, instancje internetowe sami mogą wczytywać zewnętrzne skrypty do wykorzystania w kontekście instancji roboczych – za pomocą instrukcji importScripts lub statycznych instrukcji import w przeglądarkach, które obsługują zasoby robocze modułu. W rezultacie każdy skrypt zażądany 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 jest jednym z czynników wpływających na dzielenie skryptów. Gdy skrypty są mniejsze, kompresja staje się mniej efektywna. 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 to idealne narzędzia do zarządzania rozmiarem danych wyjściowych skryptów, od których zależy działanie witryny:

  • W przypadku problemów z pakietem webpack możesz skorzystać z wtyczki SplitChunksPlugin. Informacje o opcjach ułatwiających zarządzanie rozmiarami zasobów znajdziesz w dokumentacji usługi SplitChunksPlugin.
  • W przypadku innych rozwiązań do tworzenia pakietów, takich jak Rollup i esbuild, możesz zarządzać rozmiarami plików skryptu, używając dynamicznych wywołań import() w kodzie. Te pakiety (oraz pakiet internetowy) automatycznie rozdzielą dynamicznie importowany zasób do osobnego pliku, dzięki czemu unikniesz większych początkowych rozmiarów pakietów.

Unieważnienie pamięci podręcznej

Unieważnienie pamięci podręcznej odgrywa dużą rolę w szybkości wczytywania strony przy kolejnych wizytach. Wysyłanie dużych, monolitycznych pakietów skryptów jest niekorzystne dla buforowania przeglądarki. Dzieje się tak, ponieważ podczas aktualizacji kodu własnego – przez aktualizację pakietów lub poprawki błędów związanych z dostawą – cały pakiet zostaje unieważniony i trzeba go pobrać ponownie.

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. Przekłada się to na szybsze wczytywanie strony.

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

Jeśli wysyłasz moduły ES w środowisku produkcyjnym i wczytujesz je za pomocą atrybutu type=module, musisz pamiętać, że zagnieżdżenie modułów może wpłynąć na czas uruchamiania. Zagnieżdżanie modułu odnosi się do sytuacji, gdy moduł ES statycznie importuje inny moduł 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ą 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. Aby tego uniknąć, możesz użyć narzędzia do tworzenia pakietów, ale upewnij się, że dzielisz skrypty w taki sposób, aby rozłożyć zadania związane z oceną skryptów.

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. Zależy to od wymagań i ograniczeń witryny. Jednak rozdzielenie skryptów powoduje rozłożenie pracy oceny skryptu na wiele mniejszych zadań, dzięki czemu wątek główny ma skuteczniejszą obsługę interakcji użytkowników, zamiast blokować wątek główny.

Oto kilka rzeczy, które możesz zrobić, aby rozdzielić duże zadania oceny skryptu:

  • Podczas wczytywania skryptów z elementem <script> bez atrybutu type=module unikaj wczytywania skryptów, które są bardzo duże, ponieważ spowodują one rozpoczęcie zadań wymagających dużej ilości zasobów, 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 natywnego wczytywania modułów ES w przeglądarce spowoduje rozpoczęcie poszczególnych zadań w celu oceny poszczególnych skryptów.
  • Zmniejsz rozmiar początkowych grup, używając dynamicznych wywołań import(). Działa to też w przypadku pakietów SDK, ponieważ traktują każdy dynamicznie zaimportowany moduł jako „punkt podziału”, co powoduje wygenerowanie osobnego skryptu dla każdego dynamicznie importowanego modułu.
  • Pamiętaj, aby wziąć pod uwagę kompromisy, takie jak wydajność kompresji czy unieważnienie pamięci podręcznej. Większe skrypty kompresują się lepiej, ale mogą wymagać 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.

Z pewnością wymaga to równowagi. Jednak dzięki rozdzieleniu skryptów i zmniejszeniu ładunków początkowych za pomocą dynamicznego import() możesz uzyskać lepszą wydajność uruchamiania i lepiej dostosować się do interakcji użytkowników w tym kluczowym okresie uruchamiania. Dzięki temu możesz uzyskać lepszy wynik wskaźnika INP, a przez to zadbać o wrażenia użytkowników.

Baner powitalny z filmu Unsplash, autorstwa Markus Spiske.