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:
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 fragmentframeworks
- 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
.
Ś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.
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.
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}`)) || []
)
}
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% |
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% |
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
.