Szybsze wczytywanie stron Next.js i Gatsby dzięki szczegółowemu podziałowi na segmenty

Nowsza strategia fragmentacji pakietu internetowego w Next.js i Gatsby minimalizuje zduplikowany kod, aby poprawić wydajność wczytywania strony.

Chrome współpracuje z narzędziami i ramkami w ekosystemie open source JavaScript. Ostatnio dodaliśmy kilka nowszych optymalizacji, aby zwiększyć szybkość wczytywania stron Next.js i Gatsby. W tym artykule omawiamy udoskonaloną strategię szczegółowego podziału na segmenty, która jest obecnie udostępniana domyślnie w obu platformach.

Wstęp

Podobnie jak wiele platform internetowych, Next.js i Gatsby korzystają z pakietu webpack jako głównego pakietu SDK. Wprowadzono CommonsChunkPlugin, aby moduły wyjściowe współużytkowane przez różne punkty wejścia w jednym (lub kilku) fragmentach „wspólnych”. Udostępniony kod można pobrać oddzielnie i na początku przechowywać w pamięci podręcznej przeglądarki, co może przyspieszyć wczytywanie.

Ten wzorzec stał się popularny, ponieważ wiele jednostronicowych platform aplikacji przyjmowało konfigurację punktu wejścia i pakietu, która wyglądała tak:

Wspólna konfiguracja punktu wejścia i pakietu

Chociaż jest to praktyczne, koncepcja łączenia całego kodu modułów udostępnianych w jeden fragment ma swoje ograniczenia. Moduły, które nie są udostępniane w każdym punkcie wejścia, można pobrać na trasy, które z nich nie korzystają, co skutkuje pobraniem większej ilości kodu, niż jest to konieczne. Na przykład: gdy page1 wczytuje fragment common, wczytuje kod dla moduleC, mimo że page1 nie używa moduleC. Z tego powodu, podobnie jak kilka innych, pakiet webpack w wersji 4 usunął tę wtyczkę na nową: SplitChunksPlugin.

Ulepszone częściowanie

Domyślne ustawienia funkcji SplitChunksPlugin sprawdzają się u większości użytkowników. Aby uniemożliwić pobieranie zduplikowanego kodu w wielu trasach, można utworzyć wiele podzielonych fragmentów w zależności od liczby conditions.

Jednak wiele platform internetowych korzystających z tej wtyczki jest zgodne z podejściem typu „single-commons” w przypadku podziału fragmentów. Na przykład Next.js wygeneruje pakiet commons zawierający dowolny moduł używany na ponad 50% stron i wszystkie zależności platformy (react, react-dom itd.).

const splitChunksConfigs = {
  …
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

Chociaż umieszczenie kodu zależnego od platformy we wspólnym fragmencie oznacza, że można go pobrać i zapisać w pamięci podręcznej w dowolnym punkcie początkowym, algorytm heurystyczny oparty na wykorzystaniu typowych modułów używanych na ponad połowie stron nie jest zbyt skuteczny. Zmodyfikowanie tego współczynnika może mieć tylko jeden z dwóch wyników:

  • Zmniejszenie tego współczynnika spowoduje pobranie większej ilości zbędnego kodu.
  • Jeśli zwiększysz współczynnik, więcej kodu zostanie powielonych na wielu trasach.

Aby rozwiązać ten problem, serwer Next.js zastosował inną konfigurację dla SplitChunksPlugin, która ogranicza ilość zbędnego kodu w przypadku każdej trasy.

  • Każdy wystarczająco duży moduł zewnętrzny (ponad 160 KB) jest dzielony na własny fragment.
  • Na potrzeby zależności platformy (react, react-dom itd.) tworzony jest oddzielny fragment frameworks
  • Utworzono dowolną liczbę udostępnionych fragmentów (maksymalnie 25)
  • Minimalny rozmiar wygenerowanego fragmentu zmienia się na 20 KB

Taka szczegółowa strategia podziału danych przynosi te korzyści:

  • Skrócony czas wczytywania strony Wysyłanie wielu udostępnionych fragmentów zamiast jednego ogranicza ilość zbędnego (lub zduplikowanego) kodu dla dowolnego punktu wejścia.
  • Ulepszone buforowanie podczas nawigacji. Podział dużych bibliotek i zależności platformy na osobne fragmenty zmniejsza ryzyko unieważnienia pamięci podręcznej, ponieważ w obu przypadkach raczej nie zmienią się do czasu uaktualnienia.

Możesz zobaczyć całą konfigurację zastosowaną przez Next.js w narzędziu webpack-config.ts.

Więcej żądań HTTP

Zasób SplitChunksPlugin zdefiniował podstawę szczegółowego podziału, a zastosowanie tego podejścia w przypadku platform takich jak Next.js nie było niczym nowym. Wiele platform nadal z kilku powodów nadal korzystało z jednej strategii heurystycznej i typu „commons” (pakiety). Obejmuje to obawy, że większa liczba żądań HTTP może negatywnie wpłynąć na wydajność witryny.

Przeglądarki mogą otwierać tylko ograniczoną liczbę połączeń TCP z jednym punktem początkowym (6 w Chrome), więc zminimalizowanie liczby fragmentów wysyłanych przez pakiet może zapewnić, że łączna liczba żądań nie przekroczy tego progu. Dotyczy to jednak tylko protokołów HTTP/1.1. Multipleksowanie w HTTP/2 umożliwia równoległe przesyłanie wielu żądań za pomocą 1 połączenia z jednym źródłem. Innymi słowy, zwykle nie musimy się martwić o ograniczenie liczby fragmentów wysyłanych przez nasz pakiet.

Wszystkie główne przeglądarki obsługują HTTP/2. Zespoły Chrome i Next.js chciały sprawdzić, czy zwiększenie liczby żądań przez podzielenie pojedynczego pakietu „commons” Next.js na kilka współdzielonych fragmentów wpłynie na wydajność wczytywania w jakikolwiek sposób. Zaczęła od pomiaru wydajności pojedynczej witryny i zmodyfikowania maksymalnej liczby żądań równoległych za pomocą właściwości maxInitialRequests.

Wydajność wczytywania stron ze zwiększoną liczbą żądań

Średnio w przypadku 3 uruchomień kilku prób na 1 stronie internetowej parametry load, start-render i pierwsze wyrenderowanie treści pozostawały mniej więcej takie same przy różnych maksymalnej liczbie żądań początkowych (od 5 do 15). Co ciekawe, po agresywnym podziale na setki żądań odnotowaliśmy niewielki spadek wydajności.

Wydajność wczytywania stron dzięki setkom żądań

Pokazało to, że utrzymanie poniżej wiarygodnego progu (20–25 żądań) pozwoliło znaleźć odpowiednią równowagę między wydajnością ładowania a wydajnością buforowania. Po przeprowadzeniu testów podstawowych wybrano 25 jako liczbę maxInitialRequest.

Zmiana maksymalnej liczby żądań realizowanych równolegle spowodowała powstanie więcej niż 1 udostępnionego pakietu i odpowiednie rozdzielenie ich dla poszczególnych punktów wejścia znacznie obniżyło ilość zbędnego kodu dla tej samej strony.

Redukcja ładunku JavaScript dzięki większemu fragmentacji

Eksperyment polegał tylko na zmianie liczby żądań, by sprawdzić, czy wpłynie to negatywnie na szybkość wczytywania strony. Wyniki sugerują, że ustawienie parametru maxInitialRequests na 25 na stronie testowej było optymalne, ponieważ zmniejszyło rozmiar ładunku JavaScript bez spowalniania działania strony. Całkowita ilość kodu JavaScript, która była potrzebna do nawilżania strony, pozostała bez zmian, co wyjaśnia, dlaczego szybkość wczytywania strony nie zwiększyła się mimo mniejszej ilości kodu.

Pakiet internetowy wykorzystuje 30 KB jako domyślny minimalny rozmiar do wygenerowania fragmentu. Jednak połączenie wartości maxInitialRequests 25 z minimalnym rozmiarem 20 KB spowodowało jednak lepsze buforowanie.

Zmniejszanie rozmiaru za pomocą szczegółowych fragmentów

Wiele platform, w tym Next.js, korzysta z routingu po stronie klienta (obsługiwanego przez JavaScript) do wstrzykiwania nowych tagów skryptu przy każdym przejściu trasy. Jak jednak mogą z wyprzedzeniem określić te dynamiczne fragmenty w czasie tworzenia?

Next.js używa pliku manifestu kompilacji po stronie serwera do określania, które wyjściowe fragmenty są używane w różnych punktach wejścia. Aby udostępnić te informacje również klientowi, utworzono skrócony plik manifestu kompilacji po stronie klienta w celu mapowania wszystkich zależności każdego punktu wejścia.

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Dane wyjściowe wielu współdzielonych fragmentów w aplikacji Next.js.

Ta nowsza strategia podziału na segmenty została po raz pierwszy wdrożona w Next.js za flagą, gdzie została przetestowana z udziałem wielu wczesnych użytkowników. Wielu z nich odnotowało znaczne zmniejszenie całkowitej ilości kodu JavaScript w całej witrynie:

Strona Całkowita zmiana JS % różnicy
https://www.barnebys.com/ –238 KB –23%
https://sumup.com/ –220 KB –30%
https://www.hashicorp.com/ –11 MB –71%
Zmniejszenie rozmiaru pliku JavaScript – we wszystkich trasach (skompresowane)

Ostateczna wersja została wysłana domyślnie w wersji 9.2.

Gatsby

W Gatsby definiowano typowe moduły, stosując tę samą metodę heurystyki opartej na wykorzystaniu:

config.optimization = {
  …
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

Optymalizując konfigurację pakietu internetowego pod kątem zastosowania podobnej szczegółowej strategii podziału na fragmenty, zauważyła znaczne spadki liczby JavaScriptu w wielu dużych witrynach:

Strona Całkowita zmiana JS % różnicy
https://www.gatsbyjs.org/ –680 KB –22%
https://www.thirdandgrove.com/ -390 KB -25%
https://ghost.org/ -1,1 MB –35%
https://reactjs.org/ –80 KB -8%
Zmniejszenie rozmiaru pliku JavaScript – we wszystkich trasach (skompresowane)

Zapoznaj się z opisem PR, aby dowiedzieć się, w jaki sposób klient wdrożył tę logikę w konfiguracji pakietu internetowego, która jest domyślnie obsługiwana w wersji 2.20.7.

Podsumowanie

Dostawa szczegółowych fragmentów nie dotyczy tylko Next.js, Gatsby czy nawet pakietów internetowych. Każdy użytkownik powinien rozważyć ulepszenie strategii podziału aplikacji na części, jeśli jest ona zgodna z dużym pakietem „commons” (niezależnie od używanej platformy lub pakietu modułów).

  • Jeśli chcesz zobaczyć, jak te same optymalizacje podziału zastosowano w aplikacji Vanilla React, obejrzyj tę przykładową aplikację React. Wykorzystuje ona uproszczoną wersję strategii szczegółowego podziału na fragmenty i może pomóc Ci zastosować tę samą logikę w witrynie.
  • W przypadku o pełnym zakresie fragmenty są domyślnie tworzone szczegółowo. Jeśli chcesz ręcznie skonfigurować działanie, zapoznaj się z elementem manualChunks.