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

Nowsza strategia dzielenia pakietów webpack w Next.js i Gatsby minimalizuje duplikowanie kodu, aby poprawić wydajność wczytywania strony.

Chrome współpracuje z narzędziami i platformami w ekosystemie open source JavaScript. Niedawno dodaliśmy kilka nowych optymalizacji, aby poprawić wydajność wczytywania Next.jsGatsby. W tym artykule opisujemy ulepszoną strategię dzielenia na mniejsze części, która jest teraz domyślnie stosowana w obu platformach.

Wprowadzenie

Podobnie jak wiele platform internetowych, Next.js i Gatsby używają webpacka jako podstawowego narzędzia do łączenia plików. W webpacku w wersji 3 wprowadzono CommonsChunkPlugin, aby umożliwić generowanie modułów udostępnianych między różnymi punktami wejścia w jednym (lub kilku) wspólnym fragmencie kodu. Wspólny kod można pobrać osobno i wcześniej zapisać w pamięci podręcznej przeglądarki, co może zwiększyć szybkość wczytywania.

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

Typowy punkt wejścia i konfiguracja pakietu

Koncepcja łączenia całego kodu modułu udostępnionego w jednym bloku jest praktyczna, ale ma swoje ograniczenia. Moduły, które nie są udostępniane w każdym punkcie wejścia, można pobrać w przypadku tras, które ich nie używają, co powoduje pobranie 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 i kilku innych powodów w webpacku w wersji 4 usunięto wtyczkę na rzecz nowej: SplitChunksPlugin.

Ulepszone dzielenie na części

Ustawienia domyślne SplitChunksPlugin sprawdzają się w przypadku większości użytkowników. Wiele podzielonych fragmentów jest tworzonych w zależności od liczby warunków, aby zapobiec pobieraniu zduplikowanego kodu w wielu trasach.

Wiele platform internetowych, które korzystają z tej wtyczki, nadal stosuje jednak podejście „single-commons” do dzielenia fragmentów. Na przykład Next.js wygeneruje pakiet commons, który będzie zawierać wszystkie moduły używane na ponad 50% stron oraz wszystkie zależności platformy (react, react-dom itp.).

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 w udostępnionym bloku kodu zależnego od frameworka oznacza, że można go pobrać i zapisać w pamięci podręcznej dla dowolnego punktu wejścia, heurystyka oparta na użyciu, która obejmuje wspólne moduły używane na ponad połowie stron, nie jest zbyt skuteczna. Zmiana tego współczynnika może przynieść tylko 2 rezultaty:

  • Jeśli zmniejszysz ten współczynnik, pobieranych będzie więcej niepotrzebnego kodu.
  • Jeśli zwiększysz ten współczynnik, więcej kodu zostanie powielone na różnych trasach.

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

  • Każdy wystarczająco duży moduł innej firmy (większy niż 160 KB) jest dzielony na osobny fragment.
  • Dla zależności platformy (react, react-dom itp.) tworzony jest osobny fragment frameworks.
  • Tworzonych jest tyle udostępnionych fragmentów, ile jest potrzebnych (maksymalnie 25).
  • Minimalny rozmiar fragmentu, który ma zostać wygenerowany, został zmieniony na 20 KB.

Ta szczegółowa strategia dzielenia na części zapewnia te korzyści:

  • Czas wczytywania strony jest krótszy. Wysyłanie wielu wspólnych fragmentów zamiast jednego minimalizuje ilość niepotrzebnego (lub zduplikowanego) kodu dla każdego punktu wejścia.
  • Ulepszone buforowanie podczas nawigacji. Podzielenie dużych bibliotek i zależności platformy na osobne części zmniejsza prawdopodobieństwo unieważnienia pamięci podręcznej, ponieważ obie te części rzadko się zmieniają, dopóki nie zostanie przeprowadzona aktualizacja.

Całą konfigurację przyjętą przez Next.js możesz zobaczyć w webpack-config.ts.

Więcej żądań HTTP

SplitChunksPlugin zdefiniował podstawy szczegółowego dzielenia na części, a zastosowanie tego podejścia do platformy takiej jak Next.js nie było całkowicie nową koncepcją. Wiele platform nadal jednak stosowało jedną heurystykę i strategię pakietu „commons” z kilku powodów. Obejmuje to obawę, że znacznie 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 źródłem (w przypadku Chrome jest to 6), więc zminimalizowanie liczby fragmentów wyjściowych modułu pakującego może zapewnić, że łączna liczba żądań nie przekroczy tego progu. Dotyczy to jednak tylko protokołu HTTP/1.1. Multipleksowanie w HTTP/2 umożliwia przesyłanie strumieniowe wielu żądań równolegle przy użyciu jednego połączenia z jednym źródłem. Inaczej mówiąc, nie musimy się zwykle martwić ograniczaniem liczby fragmentów emitowanych przez nasz moduł.

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 udostępnionych fragmentów wpłynie w jakikolwiek sposób na wydajność ładowania. Zaczęli od pomiaru skuteczności pojedynczej witryny, modyfikując maksymalną liczbę równoległych żądań za pomocą właściwości maxInitialRequests.

Wydajność wczytywania stron przy zwiększonej liczbie żądań

Średnio w 3 seriach wielu prób na jednej stronie internetowej czasy load, start-renderpierwszego wyrenderowania treści pozostawały mniej więcej takie same przy zmianie maksymalnej liczby początkowych żądań (od 5 do 15). Co ciekawe, niewielki narzut na wydajność zauważyliśmy dopiero po agresywnym podzieleniu na setki żądań.

Wydajność wczytywania strony przy setkach żądań

Wyniki pokazały, że utrzymywanie się poniżej wiarygodnego progu (20–25 żądań) zapewnia odpowiednią równowagę między wydajnością ładowania a efektywnością buforowania. Po przeprowadzeniu testów podstawowych wybrano liczbę 25 jako maxInitialRequest.

Zmiana maksymalnej liczby równoległych żądań spowodowała powstanie więcej niż jednego wspólnego pakietu. Odpowiednie rozdzielenie ich dla każdego punktu wejścia znacznie zmniejszyło ilość niepotrzebnego kodu na tej samej stronie.

Zmniejszenie ładunku JavaScript dzięki zwiększeniu liczby fragmentów

Ten eksperyment polegał tylko na modyfikowaniu liczby żądań, aby sprawdzić, czy będzie to miało negatywny wpływ na wydajność wczytywania strony. Wyniki sugerują, że ustawienie wartości maxInitialRequests na 25 na stronie testowej było optymalne, ponieważ zmniejszyło rozmiar ładunku JavaScript bez spowalniania strony. Całkowita ilość JavaScriptu potrzebna do nawodnienia strony pozostała mniej więcej taka sama, co wyjaśnia, dlaczego wydajność wczytywania strony niekoniecznie poprawiła się wraz ze zmniejszeniem ilości kodu.

webpack używa 30 KB jako domyślnego minimalnego rozmiaru fragmentu, który ma zostać wygenerowany. Jednak połączenie wartości maxInitialRequests = 25 z minimalnym rozmiarem 20 KB przyniosło lepsze wyniki w zakresie buforowania.

Zmniejszanie rozmiaru dzięki szczegółowym fragmentom

Wiele platform, w tym Next.js, korzysta z routingu po stronie klienta (obsługiwanego przez JavaScript), aby wstawiać nowsze tagi skryptu przy każdej zmianie trasy. Jak jednak z wyprzedzeniem określa się te dynamiczne fragmenty w czasie kompilacji?

Next.js używa pliku manifestu kompilacji po stronie serwera, aby określić, które wygenerowane fragmenty są używane przez różne punkty wejścia. Aby przekazać te informacje również klientowi, utworzono skrócony plik manifestu kompilacji po stronie klienta, który mapuje wszystkie zależności dla 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 udostępnionych fragmentów w aplikacji Next.js.

Ta nowa strategia dzielenia na mniejsze części została po raz pierwszy wprowadzona w Next.js za pomocą flagi, gdzie była testowana przez wielu użytkowników wczesnej wersji. Wielu z nich odnotowało znaczne zmniejszenie całkowitej ilości kodu JavaScript używanego w całej witrynie:

Witryna 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 JavaScript we wszystkich ścieżkach (skompresowane)

Wersja ostateczna została domyślnie wprowadzona w wersji 9.2.

Gatsby

Gatsby stosował wcześniej to samo podejście, czyli heurystykę opartą na użyciu, do definiowania wspólnych modułów:

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)[\\/]/,
      },

Dzięki optymalizacji konfiguracji webpacka pod kątem podobnej strategii szczegółowego dzielenia na fragmenty zauważyli też znaczne zmniejszenie ilości JavaScriptu w wielu dużych witrynach:

Witryna 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 JavaScript we wszystkich ścieżkach (skompresowane)

Zapoznaj się z prośbą o scalenie, aby dowiedzieć się, jak zaimplementowano tę logikę w konfiguracji webpacka, która jest domyślnie dostarczana w wersji 2.20.7.

Podsumowanie

Koncepcja wysyłania szczegółowych fragmentów nie jest specyficzna dla Next.js, Gatsby ani nawet webpacka. Każdy powinien rozważyć ulepszenie strategii dzielenia aplikacji na części, jeśli stosuje podejście dużego pakietu „commons”, niezależnie od używanego frameworka lub narzędzia do pakowania modułów.

  • Jeśli chcesz zobaczyć, jak te same optymalizacje dzielenia na mniejsze części są stosowane w czystej aplikacji React, zapoznaj się z tym przykładowym projektem React. Używa on uproszczonej wersji strategii szczegółowego dzielenia na mniejsze części i może pomóc Ci w zastosowaniu podobnej logiki w Twojej witrynie.
  • W przypadku funkcji Rollup fragmenty są domyślnie tworzone w sposób szczegółowy. Jeśli chcesz ręcznie skonfigurować to działanie, zapoznaj się z tym artykułem:manualChunks