Dostosowywanie nowych funkcji kontrolowania dzielenia na fragmenty w Turbopack w Next.js 16.3
Praktyczny przegląd nowej konfiguracji turbopackChunking w Next.js 16.3, wyjaśniający, w jaki sposób maxChunkCountPerGroup i generateComponentChunks wpływają na rozmiar pliku pakietowego oraz funkcjonowanie cache’ingu.
Była 4:40 w piątek, dokładnie w momencie, gdy zamyka się laptop na weekend, kiedy rutynowe rozruchowe wdrożenie aplikacji dashboard nagle przestało być rutyną. Budowa, która zwykle trwała 40 sekund, zajęła nieco ponad trzy minuty, a plik klienta dla głównej ścieżki powiększył się o prawie 300 kb. Żadne ścieżki nie zostały zmienione, ani nie dodano żadnych nowych zależności, które mogłyby to wyjaśnić. Jedyną zmianą w tym tygodniu była niewielka aktualizacja wersji Next.js. Nastąpił wieczór spędzony na wpatrywaniu się w kartę Sieć, obserwując, jak cztery ogromne pliki JavaScript ładują się jeden po drugim, podczas gdy wcześniej to zadanie wykonywało dziesięć małych plików.
Powodem nie był błąd. Była to logika dzielenia na fragmenty w Turbopack, która funkcjonowała dokładnie zgodnie z projektem, po prostu nie w sposób odpowiadający oczekiwaniom.
Dlaczego problem dzielenia na fragmenty nigdy tak naprawdę nie został rozwiązany
To jest problem, z którym borykają się twórcy bundli od około dziesięciu lat. Wtyczka SplitChunksPlugin w Webpacku umożliwiała ustawienie takich parametrów jak minSize, maxSize i cacheGroups, których prawie nikt tak naprawdę nie rozumiał w pełni, ponieważ były one głównie skopiowane z jakiegoś tematu na Stack Overflow z 2019 roku. Podstawowy kompromis pozostaje niezmieniony: łączenie plików w mniejszą liczbę większych fragmentów zmniejsza liczbę żądań HTTP, ale pogarsza funkcjonowanie cache’u, ponieważ jedna zmiana w pliku CSS może unieważnić cały bundl o wielkości 500 kb. Dzielenie plików na wiele mniejszych fragmentów poprawia szczegółowość cache’u, ale zwiększa liczbę ruchów między serwerem a klientem, co szybko staje się odczuwalne przy wolnym połączeniu.
Turbopack odziedziczył tę samą zasadę równowagi i do niedawna silnie skłaniał się ku konsolidacji. Jest to rozsądny wybór dla sesji jednostronicowych, gdzie większość zasobów i tak jest pobierana tylko raz. Problem pojawia się jednak, gdy użytkownicy wielokrotnie przemieszczają się po dużej aplikacji, ponieważ wcześniej zapisany kod jest ciągle ponownie pobierany wraz z nowym kodem znajdującym się obok niego.
Co zmieniło się w 16.3
W Next.js 16.3 zachowanie łączenia nie jest już ukryte w jednej nieprzejrzystej heurystyce – teraz jest dostępne bezpośrednio poprzez konfigurację. Dodano nowy blok turbopackChunking, a dwoma najważniejszymi ustawieniami są maxChunkCountPerGroup oraz maxMergeChunkSize.
// next.config.js
module.exports = {
experimental: {
turbopackChunking: {
maxChunkCountPerGroup: 40, // default
maxMergeChunkSize: 200_000, // bytes, default
},
},
};
Obniżenie wartości maxChunkCountPerGroup skłania Turbopack do bardziej agresywnego łączenia kodu: mniej żądań przy załadunku strony, ale słabsze przechowywanie w pamięci podręcznej podczas nawigacji. Jej zwiększenie powoduje tworzenie mniejszych, liczniejszych fragmentów kodu, które pozostają nienaruszone nawet wtedy, gdy zmieniają się niepowiązane części aplikacji. maxMergeChunkSize określa maksymalny rozmiar fragmentu po połączeniu, zanim Turbopack przestanie dodawać do niego dalszy kod – co jest przydatne, gdy jedna duża zależność od dostawcy ma tendencję do pochłaniania wszystkiego wokół siebie.
Wcześniej żadne z tych ustawień nie istniało. Można było po prostu korzystać z wartości domyślnej i radzić sobie z jej ograniczeniami.
Opcja dzielenia na komponenty
Jeszcze bardziej istotną nowością jest generateComponentChunks, który rozszerza funkcjonalność dzielenia na fragmenty w Turbopacku o pojedyncze komponenty, zamiast ograniczać się do granic tras.
module.exports = {
experimental: {
turbopackChunking: {
generateComponentChunks: true,
},
},
};
Włączenie tej funkcji sprawia, że Turbopack wyświetla zarówno połączone, jak i nierozpołączone wersje fragmentów kodu, pozwalając środowisku uruchomieniowemu podczas ładowania zdecydować, którą wersję pobrać na podstawie tego, co już zainstalowano w pamięci przeglądarki. Jeśli komponent współdzielony został zapisany w pamięci przeglądarki po wcześniejszej wizycie na stronie, środowisko uruchomieniowe może poprosić o mniejszy, nierozpołączony fragment zamiast pobierać połączoną paczkę, która zawiera duplikaty już obecnego kodu. Jest to zasadniczo inny podejście niż próba uprzedniego obliczenia idealnej wielkości fragmentu – jest to pierwszy przypadek, gdy narzędzie do pakowania odkłada tę decyzję na moment żądania zamiast ustalać ją już podczas budowania aplikacji.
To, co faktycznie zobaczyliśmy
Zespół testujący to rozwiązanie w aplikacji typu dashboard włączył funkcję generateComponentChunks i obserwował, co się dzieje w ciągu następnych dni. Początkowe ładowanie strony prawie się nie zmieniło – zmniejszyło się jedynie o około 2%. Natomiast powtarzane nawigacje, które stanowią większość rzeczywistego użytkowania aplikacji takiej jak dashboard, spowodowały spadek średniej ilości przesyłanych plików JS przy każdej nawigacji o około 35%. To właśnie jest główna zaleta tego rozwiązania: nie chodzi tu o funkcję mającą na celu szybsze pierwsze ładowanie, lecz o rozwiązanie przydatne w sytuacji, gdy ktoś korzysta z aplikacji przez dziesięć minut bez przerwy. Większość testów wielkości plików dostępnych w Internecie dotyczy zupełnie innego scenariusza, który nie odzwierciedla rzeczywistego sposobu używania aplikacji z intensywną nawigacją wewnętrzną.
To samo zespół ponownie przeanalizował wartość maxChunkCountPerGroup dla strony marketingowej znajdującej się w tym samym monorepo, obniżając ją z 40 na 24. Ta strona charakteryzuje się głównie sesjami jednostronicowymi, więc dodatkowe żądania sieciowe wynikające z bardziej szczegółowego dzielenia na fragmenty stanowiły jedynie zbędny obciążenie bez żadnych korzyści. To ta sama podstawowa narzędziowa platforma, ale zupełnie inne ustawienia – obie opcje były właściwe w swoich konkretnych kontekstach. To chyba jest prawdziwa lekcja: nigdy nie istniała jedna uniwersalnie poprawna strategia dzielenia na fragmenty; Turbopack po prostu dokonywał tego wyboru za nas, bez pytania.
Jak to się porównuje ze starymi ustawieniami Webpacka
Dla tych, którzy lata spędzili na radzeniu sobie z splitChunks.cacheGroups, najbliższym odpowiednikiem w tym nowym systemie jest maxMergeChunkSize, a w odróżnieniu od wielu nazw używanych przez Webpack, ta funkcja zachowuje się dokładnie tak, jak powinna. Konfiguracja Webpack oferowała tuzin słabo udokumentowanych ustawień, które oddziaływały na siebie w sposób, którego nikt nie potrafił w pełni zrozumieć, a wiele zespołów odziedziczyło pliki konfiguracyjne po inżynierach, którzy już dawno odeszli – pliki, których nikt nie odważył się zmienić. Podejście Turbopack jest celowo bardziej ograniczone. Masz dwa lub trzy elementy do regulacji zamiast tuzina, co poświęca nieco elastyczności w rzadkich przypadkach, ale jednocześnie skraca odległość pomiędzy zauważeniem, że „nasz plik nie wygląda dobrze”, a wiedzą, jaką dokładnie wartość należy dostosować. Ta kompromis jest akceptowalny, szczególnie dla tych, którzy woleliby uniknąć…
Istnieje jeszcze jedna różnica, którą warto wymienić osobno. Webpack w dużej mierze zmuszał do rozważania kwestii dzielenia plików na fragmenty w czasie budowania, przyglądania się wynikom pliku stats.json i próbowania przewidzenia, co faktycznie znajdzie się w pamięci cache przeglądarki rzeczywistego użytkownika. Dzielenie plików na poziomie komponentów w Turbopack przenosi część tych rozważań na moment wysyłania żądania, dzięki czemu silnik w czasie wykonywania może sprawdzić rzeczywisty stan cache zamiast zgadywać go z góry. To prawdziwa zmiana architektoniczna, a nie tylko zmiana nazwy flagi konfiguracyjnej.
Wady
Ta funkcja nadal jest oznaczona jako eksperymentalna, a dokumentacja jasno o tym informuje. Jedna z zespołów napotkała przypadek, w którym dynamicznie importowana biblioteka do tworzenia wykresów została podzielona w taki sposób, że podczas implementacji w środowisku produkcyjnym chwilowo pojawił się błąd „failed to load chunk” – to znany problem starego odniesienia do fragmentu kodu, który dotknął praktycznie każdy narzędzie do pakowania z obsługą dzielenia kodu. Pełne odświeżenie rozwiązało ten problem u użytkowników, którzy na niego natrafili, ale stanowi to przydatne przypomnienie, że bardziej szczegółowe dzielenie kodu zwiększa pole działania tego konkretnego błędu, zamiast je zmniejszać.
Istnieje jeszcze kwestia dostosowywania tych ustawień bez rzeczywistych danych o ruchu, co w gruncie rzeczy polega na zgadujaniu. Warto oprzeć się pokusie zmiany domyślnych ustawień w nowym projekcie, dopóki nie masz rzeczywistych analiz nawigacji pokazujących, czy twoi odwiedzający są typem tych, którzy odwiedzają stronę tylko raz, czy raczej takimi, którzy w jednej sesji przemierzają dziesiątki tras. Jeśli takich danych jeszcze nie ma, to jest to równie dobry powód, by zacząć je zbierać przed modyfikowaniem jakiejkolwiek konfiguracji podziału danych. Dostosowywanie na ślepo niesie ze sobą realne ryzyko gorszego wskaźnika trafień do pamięci cache w porównaniu z tym, co dałyby domyślne ustawienia, a taka regresja przebiega bez żadnych objawów. Nikt nie składa raportu o błędzie z tytułem „Nawigacja była trochę wolniejsza” – po prostu przestają wracać.
Czy więc warto to włączyć?
W przypadku scenariusza z panelu sterowania odpowiedź była twierdząca, a ustawienie pozostało włączone. W przypadku strony marketingowej dostosowano tylko jedną liczbę i to było wszystko. Nic z tego nie naprawia fundamentalnie uszkodzonego oprogramowania, a jeśli twój pakiet jest przeciążony z powodu zależności, której tak naprawdę nie potrzebujesz, żadna strategia dzielenia na fragmenty nie pomoże ci wyjść z tego błędu. Funkcja ta oferuje skupione rozwiązanie wąskiego problemu, co stanowi orzeźwiającą odmianę w porównaniu z wieloma funkcjami narzędzi do budowania oprogramowania dostarczanymi w tym roku – ocena ta ma pewną wagę, szczególnie pochodząc od kogoś, kto spędził więcej wieczorów, niż by chciał, łapiąc właśnie tego typu błędy.
Literatura pokrewna
- Migracja API Express do handlerów tras App Router w Next.js — Dowiedz się, jak przekształcić trasy Express, middleware oraz wzorce danych na App Router w Next.js z wykorzystaniem komponentów serwerowych oraz uwag dotyczących wdrażania.
- Zamiana Jest na własnego exekutora testów Node w Node 24 — Przykład z praktyki pokazuje, jak wbudowany exekutor testów w Node 24 oraz natywna obsługa TypeScript skracają czas CI, jednocześnie eliminując cztery zależności.