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

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

Chrome współpracuje z narzędziami i ramami w ekosystemie JavaScript open source. Niedawno dodaliśmy kilka nowych optymalizacji, aby poprawić szybkość wczytywania Next.js i Gatsby. Ten artykuł opisuje ulepszoną strategię szczegółowego dzielenia na fragmenty, która jest teraz domyślnie udostępniana w obu usługach.

Wprowadzenie

Podobnie jak wiele innych frameworków internetowych, Next.js i Gatsby używają webpacka jako podstawowego narzędzia do tworzenia pakietów. Wersja 3 webpacka umożliwia wyprowadzanie modułów udostępnianych między różnymi punktami wejścia w jednym (lub kilku) fragmencie „commons”. Udostępniony kod można pobrać osobno i wcześniej zapisać w pamięci podręcznej przeglądarki, co może poprawić wydajność wczytywania.

Ten wzorzec stał się popularny w wielu ramach aplikacji jednostronicowych, które stosowały konfigurację punktu wejścia i pakietu w taki sposób:

Konfiguracja wspólnego punktu wejścia i pakietu

Chociaż jest to praktyczne, koncepcja łączenia całego udostępnionego kodu modułu w jeden fragment ma swoje ograniczenia. Moduły, które nie są udostępniane w każdym punkcie wejścia, mogą być pobierane na potrzeby tras, które ich nie używają. W efekcie pobierany jest większy kod niż to konieczne. Gdy na przykład page1 wczytuje fragment common, wczytuje kod dla moduleC, mimo że page1 nie używa moduleC. Z tego powodu wraz z kilkoma innymi pakietami Webpack w wersji 4 usunięto wtyczkę i zastąpiliśmy ją nową: SplitChunksPlugin.

Ulepszone dzielenie na fragmenty

Domyślne ustawienia 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 powtórzonego kodu na różnych trasach.

Jednak wiele frameworków internetowych, które korzystają z tego wtyczka, nadal stosuje podejście „single-commons” do podziału na fragmenty. Na przykład Next.js wygenerowałby pakiet commons zawierający dowolny moduł używany na ponad 50% stron oraz wszystkie zależności frameworka (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ż dodanie kodu zależnego od frameworku do wspólnego fragmentu oznacza, że można go pobrać i zapisać w pamięci podręcznej dla dowolnego punktu wejścia, heurystyka oparta na wykorzystaniu, która polega na dodawaniu typowych modułów używanych na ponad połowie stron, nie jest zbyt skuteczna. Zmiana tego współczynnika może mieć jeden z 2 możliwych wyników:

  • Jeśli zmniejszysz współczynnik proporcji, zostanie pobrany więcej niepotrzebnego kodu.
  • Jeśli zwiększysz współczynnik, więcej kodu zostanie powielonych na wielu trasach.

Aby rozwiązać ten problem, Next.js wprowadził inną konfigurację dla SplitChunksPlugin, która ogranicza niepotrzebny kod dla dowolnej ścieżki.

  • Każdy wystarczająco duży moduł zewnętrzny (większy niż 160 KB) jest dzielony na osobne fragmenty.
  • Dla zależności frameworka (react, react-dom itd.) tworzony jest osobny fragment frameworks.
  • Utworzono tyle udostępnionych fragmentów, ile było potrzebnych (maksymalnie 25).
  • Minimalny rozmiar generowanego fragmentu został zmieniony na 20 KB.

Ta strategia dzielenia na drobne fragmenty zapewnia następujące korzyści:

  • Czas wczytywania stron wydłużył się. Wygenerowanie wielu wspólnych fragmentów zamiast jednego pozwala zminimalizować ilość niepotrzebnego (lub duplikowanego) kodu w przypadku dowolnego punktu wejścia.
  • Poprawione buforowanie podczas nawigacji. Dzielenie dużych bibliotek i zależności od frameworka na osobne fragmenty zmniejsza prawdopodobieństwo unieważnienia pamięci podręcznej, ponieważ obie te rzeczy rzadko się zmieniają, dopóki nie nastąpi aktualizacja.

Całą konfigurację przyjętą przez Next.js znajdziesz w sekcji webpack-config.ts.

Więcej żądań HTTP

SplitChunksPlugin określił podstawę szczegółowego dzielenia na części, a zastosowanie tego podejścia w ramach takich frameworków jak Next.js nie było zupełnie nową koncepcją. Wiele frameworków nadal używało jednak jednej strategii heurystycznej i pakietu „wspólnych” z kilku powodów. Dotyczy to również obaw, że znacznie więcej żądań HTTP może negatywnie wpłynąć na wydajność witryny.

Przeglądarki mogą otwierać ograniczoną liczbę połączeń TCP z jednym punktem początkowym (6 w przypadku Chrome), dlatego zminimalizowanie liczby fragmentów generowanych przez program tworzący pakiet może sprawić, że łączna liczba żądań pozostanie poniżej tego progu. Dotyczy to jednak tylko HTTP/1.1. Multipleks w HTTP/2 umożliwia równoległe przesyłanie wielu żądań z użyciem pojedynczego połączenia w jednym źródle. Inaczej mówiąc, nie musimy się martwić ograniczaniem liczby fragmentów emitowanych 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 wiele współdzielonych fragmentów w jakikolwiek sposób wpłynie na wydajność wczytywania. Na początku zmierzono wydajność pojedynczej witryny, zmieniając maksymalną liczbę równoczesnych żądań za pomocą usługi maxInitialRequests.

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

W przypadku średnio 3 wyników z wielu prób na jednej stronie internetowej czasy load, rozpoczęcia renderowaniapierwszego wyrenderowania treści były mniej więcej takie same przy różnych wartościach maksymalnej początkowej liczby żądań (od 5 do 15). Co ciekawe, zauważyliśmy niewielki wzrost obciążenia tylko po agresywnym podzieleniu na setki żądań.

Wydajność wczytywania stron przy setkach żądań

Pokazało to, że utrzymanie równowagi między wydajnością wczytywania a wydajnością buforowania między 20–25 żądaniami nie przekraczało pewnego stabilnego progu. Po przeprowadzeniu testów bazowych wybrano wartość 25 jako maxInitialRequest.

Zmodyfikowanie maksymalnej liczby żądań realizowanych równolegle skutkowało więcej niż 1 udostępnionym pakietem, a odpowiednie rozdzielenie ich dla każdego punktu wejścia znacznie zmniejszyło ilość niepotrzebnego kodu na tej samej stronie.

Redukcja ładunku JavaScript przez zwiększenie podziału na fragmenty

Ten eksperyment polegał tylko na modyfikacji liczby żądań, aby sprawdzić, czy będzie to miało negatywny wpływ na wydajność wczytywania strony. Wyniki wskazują, że ustawienie wartości maxInitialRequests na stronie testowej na 25 było optymalne, ponieważ zmniejszyło rozmiar danych JavaScript bez spowolnienia działania strony. Łączna ilość kodu JavaScript potrzebna do odświeżenia strony pozostała mniej więcej taka sama, co wyjaśnia, dlaczego wydajność wczytywania strony niekoniecznie poprawiła się po zmniejszeniu ilości kodu.

webpack używa domyślnego minimalnego rozmiaru 30 KB dla generowanego fragmentu. Jednak połączenie wartości maxInitialRequests 25 z minimalnym rozmiarem 20 KB dało lepsze wyniki w zakresie buforowania.

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

Wiele platform, w tym Next.js, korzysta z przekierowywania po stronie klienta (obsługiwanego przez JavaScript), aby wstrzykiwać nowe tagi skryptu przy każdym przejściu na inną stronę. Jak jednak wstępnie określają te dynamiczne fragmenty na etapie kompilacji?

Next.js korzysta z pliku manifestu kompilacji po stronie serwera, aby określić, które wysyłane fragmenty są używane przez różne punkty wejścia. Aby udostępnić te informacje klientowi, utworzyliśmy 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 z wielu udostępnionych fragmentów w aplikacji Next.js.

Ta nowsza strategia szczegółowego dzielenia na fragmenty została po raz pierwszy wdrożona w Next.js za flagą, która została przetestowana na kilku wczesnych użytkownikach. Wielu z nich odnotowało znaczny spadek łącznego JavaScriptu 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 pliku JavaScript – we wszystkich trasach (skompresowanych)

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

Gatsby

Gatsby stosował wcześniej takie samo podejście do definiowania wspólnych modułów na podstawie heurystyki opartej na sposobie użycia:

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

Po optymalizacji konfiguracji webpacka pod kątem podobnej strategii dzielenia na części zauważyli oni znaczne zmniejszenie rozmiaru 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 KoB -8%
Zmniejszenie rozmiaru pliku JavaScript – we wszystkich trasach (skompresowanych)

Zapoznaj się z PR, aby dowiedzieć się, jak firma wdrożyła tę logikę w konfiguracji pakietu internetowego, który jest domyślnie dostarczany w wersji 2.20.7.

Podsumowanie

Koncepcja dostarczania szczegółowych fragmentów nie jest charakterystyczna dla Next.js, Gatsby ani nawet webpack. Każdy programista powinien rozważyć udoskonalenie strategii dzielenia aplikacji na części, jeśli stosuje on dużą metodę pakietowania „commons”, niezależnie od używanego frameworku lub modułu pakietującego.

  • Te same optymalizacje dotyczące dzielenia na fragmenty zostały zastosowane w aplikacji React na temat wanilii. Spójrz na tę przykładową aplikację React. Wykorzystuje ona uproszczoną wersję strategii szczegółowego dzielenia na fragmenty i ułatwia stosowanie tej samej logiki w witrynie.
  • W przypadku podsumowania fragmenty są domyślnie tworzone szczegółowo z zachowaniem szczegółowości. Jeśli chcesz ręcznie skonfigurować to działanie, otwórz stronę manualChunks.