Nowsza strategia dzielenia na fragmenty w webpacku w Next.js i Gatsby minimalizuje powielanie kodu, aby zwiększyć wydajność wczytywania strony.
Chrome współpracuje z narzędziami i platformami w ekosystemie open source JavaScriptu. Ostatnio dodano kilka nowszych optymalizacji, które poprawiają wydajność wczytywania w Next.js i Gatsby. W tym artykule omawiamy ulepszoną strategię dzielenia na fragmenty, która jest teraz domyślnie dostępna w obu platformach.
Wprowadzenie
Podobnie jak wiele platform internetowych, Next.js i Gatsby używają webpack jako podstawowego pakietu. W webpack w wersji 3 wprowadzono CommonsChunkPlugin, aby umożliwić generowanie modułów współdzielonych między różnymi punktami wejścia w jednym (lub kilku) „wspólnym” fragmencie (lub fragmentach). Udostępniony kod można pobrać osobno i zapisać w pamięci podręcznej przeglądarki na wczesnym etapie, co może poprawić wydajność wczytywania.
Ten wzorzec stał się popularny, gdy wiele platform aplikacji jednostronicowych przyjęło konfigurację punktu wejścia i pakietu, która wyglądała tak:

Chociaż koncepcja łączenia całego kodu modułu udostępnionego w jeden fragment jest praktyczna, ma swoje ograniczenia. Moduły, które nie są udostępniane w każdym punkcie wejścia, mogą być pobierane w przypadku tras, które ich nie używają, co powoduje pobieranie większej ilości kodu niż jest to konieczne. Na przykład gdy page1 wczytuje fragment common, wczytuje też kod moduleC, mimo że page1 nie używa moduleC.
Z tego i kilku innych powodów w webpacku w wersji 4 usunięto tę wtyczkę i zastąpiono ją nową: SplitChunksPlugin.
Ulepszone dzielenie na fragmenty
Domyślne ustawienia SplitChunksPlugin sprawdzają się w przypadku większości użytkowników. W zależności od wielu warunków tworzonych jest wiele podzielonych fragmentów, aby zapobiec pobieraniu zduplikowanego kodu na wielu trasach.
Jednak wiele platform internetowych, które używają tej wtyczki, nadal stosuje podejście „single-commons” do dzielenia fragmentów. Na przykład Next.js wygeneruje pakiet commons, który zawiera wszystkie moduły używane na ponad 50% stron oraz 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 dla dowolnego punktu wejścia, heurystyka oparta na wykorzystaniu, polegająca na dołączaniu wspólnych modułów używanych na ponad połowie stron, nie jest zbyt skuteczna. Zmiana tego współczynnika może przynieść tylko jeden z 2 rezultatów:
- Jeśli zmniejszysz współczynnik, pobieranych będzie więcej niepotrzebnego kodu.
- Jeśli zwiększysz ten współczynnik, więcej kodu zostanie zduplikowane na wielu trasach.
Aby rozwiązać ten problem, Next.js przyjął inną konfigurację dla SplitChunksPlugin, która zmniejsza niepotrzebny kod w przypadku każdej trasy.
- Każdy wystarczająco duży moduł innej firmy (większy niż 160 KB) jest dzielony na osobne fragmenty.
- Dla zależności platformy (
react,react-domitd.) tworzony jest osobny fragmentframeworks. - Tworzona jest dowolna liczba udostępnionych fragmentów (maksymalnie 25).
- Minimalny rozmiar generowanego fragmentu został zmieniony na 20 KB.
Ta szczegółowa strategia fragmentacji zapewnia te korzyści:
- Skrócenie czasu wczytywania stron. Emitowanie wielu udostępnionych fragmentów zamiast jednego minimalizuje ilość niepotrzebnego (lub zduplikowanego) kodu w każdym punkcie wejścia.
- Ulepszone buforowanie podczas nawigacji. Podział dużych bibliotek i zależności platformy na osobne fragmenty zmniejsza prawdopodobieństwo unieważnienia pamięci podręcznej, ponieważ jest mało prawdopodobne, że obie te rzeczy ulegną zmianie przed uaktualnieniem.
Całą konfigurację przyjętą przez Next.js możesz zobaczyć w pliku webpack-config.ts.
Więcej żądań HTTP
SplitChunksPlugin zdefiniował podstawy szczegółowego dzielenia na fragmenty, a zastosowanie tego podejścia do platformy takiej jak Next.js nie było zupełnie nową koncepcją. Jednak wiele platform nadal korzystało z jednej heurystyki i strategii pakietu „commons” z kilku powodów. Dotyczy to m.in. obaw, ż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 źródłem (6 w przypadku Chrome), więc zminimalizowanie liczby fragmentów wyjściowych przez bundler 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 wielu żądań równolegle za pomocą jednego połączenia z jednego źródła. Innymi słowy, zwykle nie musimy się martwić o ograniczenie liczby fragmentów emitowanych przez nasz bundler.
Wszystkie główne przeglądarki obsługują protokół 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 udostępnionych fragmentów wpłynie w jakikolwiek sposób na wydajność wczytywania. Zaczęli od pomiaru wydajności pojedynczej witryny, modyfikując maksymalną liczbę żądań równoległych za pomocą właściwości maxInitialRequests.

W średniej z 3 przebiegów wielu prób na jednej stronie internetowej czas load, rozpoczęcia renderowania i pierwszego wyrenderowania treści pozostał mniej więcej taki sam przy zmianie maksymalnej liczby żądań początkowych (od 5 do 15). Co ciekawe, zauważyliśmy niewielki spadek wydajności dopiero po agresywnym podziale na setki żądań.

Wykazały one, że utrzymanie liczby żądań poniżej niezawodnego progu (20–25 żądań) zapewnia właściwą równowagę między wydajnością wczytywania a efektywnością buforowania. Po przeprowadzeniu testów podstawowych jako liczbę maxInitialRequest wybrano 25.
Zmiana maksymalnej liczby żądań, które mogą być wysyłane równolegle, spowodowała, że powstało więcej niż 1 udostępniony pakiet, a odpowiednie ich rozdzielenie dla każdego punktu wejścia znacznie zmniejszyło ilość niepotrzebnego kodu na tej samej stronie.

Ten eksperyment dotyczył tylko zmiany liczby żądań, aby sprawdzić, czy będzie to miało negatywny wpływ na wydajność wczytywania strony. Wyniki sugerują, że ustawienie maxInitialRequests na 25 na stronie testowej było optymalne, ponieważ zmniejszyło rozmiar ładunku JavaScriptu bez spowalniania strony. Łączna ilość kodu JavaScript potrzebnego do hydratacji 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 30 KB jako domyślnego minimalnego rozmiaru generowanego fragmentu. Jednak połączenie wartości maxInitialRequests równej 25 z minimalnym rozmiarem 20 KB zapewniło lepsze buforowanie.
Zmniejszenie rozmiaru dzięki szczegółowym fragmentom
Wiele platform, w tym Next.js, korzysta z routingu po stronie klienta (obsługiwanego przez JavaScript), aby wstrzykiwać nowsze tagi skryptu przy każdym przejściu trasy. Jak jednak określić te dynamiczne fragmenty na etapie 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 udostępnić 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}`)) || []
)
}
Ta nowsza strategia szczegółowego dzielenia na fragmenty została najpierw wprowadzona w Next.js za flagą, gdzie została przetestowana na wielu użytkownikach wczesnej wersji. Wielu z nich zauważyło znaczne zmniejszenie ilości kodu JavaScript używanego w całej witrynie:
| Witryna | Łączna 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 domyślnie udostępniona w wersji 9.2.
Gatsby
Gatsby stosował to samo podejście polegające na użyciu heurystyki opartej na wykorzystaniu 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 udało się też znacznie zmniejszyć ilość kodu JavaScript w wielu dużych witrynach:
| Witryna | Łączna 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 PR, aby dowiedzieć się, jak ta logika została zaimplementowana w konfiguracji webpack, 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 fragmenty, jeśli stosuje podejście z dużym pakietem „commons”, niezależnie od używanego frameworka lub pakietu modułów.
- Jeśli chcesz zobaczyć te same optymalizacje fragmentacji zastosowane w zwykłej aplikacji React, zapoznaj się z tą przykładową aplikacją React. Wykorzystuje ona uproszczoną wersję strategii szczegółowej fragmentacji i może pomóc Ci zacząć stosować ten sam rodzaj logiki w Twojej witrynie.
- W przypadku podsumowania fragmenty są domyślnie tworzone szczegółowo. Jeśli chcesz ręcznie skonfigurować działanie, zapoznaj się z
manualChunks.