Zadaj pytanie i otrzymaj streszczenie dokumentu, odwołując się do tej strony i wybranego dostawcy AI
Historia wersji
- "Dodaj porównanie gwiazdek GitHub"v8.9.818.05.2026
- "Inicjalizacja benchmarka"v8.7.56.01.2026
Treść tej strony została przetłumaczona przy użyciu sztucznej inteligencji.
Zobacz ostatnią wersję oryginalnej treści w języku angielskimJeśli masz pomysł na ulepszenie tej dokumentacji, zachęcamy do przesłania pull requesta na GitHubie.
Link do dokumentacji na GitHubieKopiuj dokument Markdown do schowka
Biblioteki i18n dla Next.js - Raport Benchmark 2026
Ta strona to raport z benchmarku rozwiązań i18n w Next.js.
Spis Treści
Interaktywny Benchmark
Dynamiczne ładowanie JSON
Wczytuje tłumaczenia leniwie w czasie wykonywania
Ograniczony JSON (przestrzenie nazw)
Przestrzenie nazw tłumaczeń na stronę
Benchmark wydajności I18n
Czym jest ta metryka?
Całkowity skompresowany przez gzip rozmiar pakietu biblioteki umiędzynarodowienia. Obejmuje tylko dostawcę i logikę pobierania treści po tree-shakingu i minifikacji.
Dlaczego to jest ważne?
Mniejszy rozmiar biblioteki zmniejsza obciążenie u klienta.
Zobacz jako
Referencja wyników:
Zobacz pełne dane benchmarku
Zobacz pełne repozytorium benchmarka tutaj.
Wprowadzenie
Biblioteki internacjonalizacji mają ogromny wpływ na Twoją aplikację. Głównym ryzykiem jest ładowanie treści dla każdej strony i każdego języka, gdy użytkownik odwiedza tylko jedną stronę.
W miarę rozwoju aplikacji rozmiar pakietu może rosnąć wykładniczo, co może zauważalnie obniżyć wydajność.
Jako przykład: w najgorszych przypadkach, po internacjonalizacji Twoja strona może stać się niemal 4 razy większa.
Innym skutkiem bibliotek i18n jest wolniejszy rozwój. Przekształcanie komponentów w wielojęzyczne treści w wielu językach jest czasochłonne.
Ponieważ problem jest trudny, istnieje wiele rozwiązań - niektóre koncentrują się na DX (Developer Experience), inne na wydajności lub skalowalności itd.
Intlayer stara się optymalizować we wszystkich tych wymiarach.
TL;DR
- Intlayer i next-translate: Najlepsze wybory dla wydajności Next.js, oferujące najmniejszy rozmiar i najlepsze wsparcie dla statycznego renderowania.
- next-intl: Najmodniejsza obecnie opcja, ale ciężka i skomplikowana w optymalizacji dla dużych aplikacji.
- next-i18next: Popularna i bogata w pluginy, ale niesie ze sobą znaczny ciężar pakietu (~3× Intlayer).
- Unikaj: gt-next i lingo.dev ze względu na poważne problemy z wydajnością, uzależnienie od dostawcy (vendor lock-in) i błędy blokujące budowanie.
Przetestuj swoją aplikację
Aby ujawnić te problemy, zbudowałem darmowy skaner, który możesz wypróbować tutaj.
Problem
Istnieją dwa główne sposoby na ograniczenie wpływu aplikacji wielojęzycznej na rozmiar pakietu:
- Podział plików JSON (lub treści) na pliki / zmienne / przestrzenie nazw (namespaces), aby bundler mógł usunąć (tree-shake) nieużywaną treść dla danej strony.
- Dynamiczne ładowanie treści strony tylko w języku użytkownika.
Ograniczenia techniczne tych podejść:
Ładowanie dynamiczne
Nawet gdy deklarujesz ścieżki typu [locale]/page.tsx, korzystając z Webpacka lub Turbopacka, i nawet jeśli zdefiniowano generateStaticParams, bundler nie traktuje locale jako stałej statycznej. Oznacza to, że może on pobrać treść dla wszystkich języków do każdej strony. Głównym sposobem na ograniczenie tego jest ładowanie treści poprzez import dynamiczny (np. import('./locales/${locale}.json')).
To, co dzieje się w czasie budowania (build time), to fakt, że Next.js generuje jeden pakiet JS na lokalizację (np. ./locales_pl_12345.js). Po wysłaniu strony do klienta, gdy strona jest uruchamiana, przeglądarka wykonuje dodatkowe żądanie HTTP o potrzebny plik JS (np. ./locales_pl_12345.js).
Innym sposobem na rozwiązanie tego samego problemu jest użyciefetch()do dynamicznego ładowania plików JSON. Tak działaTolgee, gdy pliki JSON znajdują się w/public, lubnext-translate, który polega nagetStaticPropsdo ładowania treści. Przepływ jest taki sam: przeglądarka wysyła dodatkowe żądanie HTTP, aby załadować zasób.
Podział treści (Content splitting)
Jeśli używasz składni typu const t = useTranslation() + t('moj-obiekt.moj-podobiekt.moj-klucz'), cały plik JSON musi zazwyczaj znajdować się w pakiecie, aby biblioteka mogła go przeanalizować i rozwiązać klucz. Duża część tej treści jest wysyłana nawet wtedy, gdy nie jest używana na stronie.
Aby to złagodzić, niektóre biblioteki proszą o zadeklarowanie na poziomie strony, które przestrzenie nazw należy załadować - np. next-i18next, next-intl, lingui, next-translate, next-international.
Z kolei Paraglide dodaje dodatkowy krok przed budowaniem, aby zamienić pliki JSON na proste symbole, takie jak const en_my_var = () => 'moja wartosc'. Teoretycznie umożliwia to usuwanie (tree-shaking) nieużywanej treści na stronie. Jak zobaczymy, ta metoda wciąż ma swoje wady.
Wreszcie, Intlayer stosuje optymalizację w czasie budowania, dzięki czemu useIntlayer('moj-klucz') jest zastępowany bezpośrednio odpowiednią treścią.
Metodologia
W tym benchmarku porównaliśmy następujące biblioteki:
Base App(Brak biblioteki i18n)next-intlayer(v8.7.12)next-i18next(v16.0.5)next-intl(v4.9.1)@lingui/core(v5.3.0)next-translate(v3.1.2)next-international(v1.3.1)@inlang/paraglide-js(v2.15.1)@tolgee/react(v7.0.0)@lingo.dev/compiler(v0.4.0)wuchale(v0.22.11)gt-next(v6.16.5)
Użyłem Next.js w wersji 16.2.4 z App Routerem.
Zbudowałem wielojęzyczną aplikację z 10 stronami i 10 językami.
Porównałem cztery strategie ładowania:
| Strategia | Brak przestrzeni nazw (globalna) | Z przestrzeniami nazw (scoped) |
|---|---|---|
| Ładowanie statyczne | Static: Wszystko w pamięci przy starcie. | Scoped static: Podział na przestrzenie nazw; wszystko ładowane przy starcie. |
| Ładowanie dynamiczne | Dynamic: Ładowanie na żądanie na lokalizację. | Scoped dynamic: Szczegółowe ładowanie na przestrzeń nazw i lokalizację. |
Podsumowanie strategii
- Static: Prosta; brak opóźnień sieciowych po początkowym załadowaniu. Minus: duży rozmiar pakietu.
- Dynamic: Redukuje początkową wagę (lazy-loading). Idealna w przypadku wielu lokalizacji.
- Scoped static: Utrzymuje porządek w kodzie (separacja logiczna) bez skomplikowanych dodatkowych żądań sieciowych.
- Scoped dynamic: Najlepsze podejście pod kątem dzielenia kodu (code splitting) i wydajności. Minimalizuje zużycie pamięci, ładując tylko to, czego potrzebuje bieżący widok i aktywna lokalizacja.
Co mierzyłem:
Uruchomiłem tę samą wielojęzyczną aplikację w prawdziwej przeglądarce dla każdego stosu technologicznego, a następnie zanotowałem, co faktycznie zostało wysłane przez sieć i ile czasu zajęły operacje. Rozmiary są podawane po normalnej kompresji internetowej, ponieważ jest to bliższe temu, co faktycznie pobierają użytkownicy.
Rozmiar biblioteki internacjonalizacji: Po spakowaniu, tree-shakingu i minifikacji, rozmiar biblioteki i18n to rozmiar kodu dostawców (np.
NextIntlClientProvider) + hooków (np.useTranslations) w pustym komponencie. Nie obejmuje ładowania plików tłumaczeń. Odpowiada na pytanie, jak "droga" jest biblioteka, zanim Twoja treść znajdzie się w projekcie.JavaScript na stronę: Dla każdej trasy benchmarku, ile skryptów przeglądarka pobiera podczas tej wizyty, uśrednione dla wszystkich stron w zestawie (i we wszystkich lokalizacjach). Ciężkie strony to wolne strony.
Wyciek z innych lokalizacji (Leakage): To treść tej samej strony, ale w innym języku, która została omyłkowo załadowana na badanej stronie. Treść ta jest zbędna i należy jej unikać (np. treść strony
/fr/aboutw pakiecie strony/en/about).Wyciek z innych ścieżek: Ta sama idea dla innych ekranów w aplikacji: czy ich teksty są dołączone, gdy otworzyłeś tylko jedną stronę (np. treść strony
/en/aboutw pakiecie strony/en/contact). Wysoki wynik sugeruje słaby podział lub zbyt szerokie pakiety.Średni rozmiar pakietu komponentu: Typowe elementy interfejsu są mierzone pojedynczo, zamiast ukrywać się wewnątrz jednej gigantycznej liczby dla aplikacji. Pokazuje to, czy internacjonalizacja po cichu „pompuje” codzienne komponenty. Na przykład, jeśli Twój komponent renderuje się ponownie, załaduje wszystkie te dane z pamięci. Dołączanie gigantycznego pliku JSON do dowolnego komponentu jest jak podłączanie wielkiego magazynu nieużywanych danych, który spowolni wydajność Twoich komponentów.
Responsywność zmiany języka: Przełączam język za pomocą własnego mechanizmu aplikacji i mierzę czas, aż strona wyraźnie się przełączy - to, co zauważyłby użytkownik, a nie mikrokrok laboratoryjny.
Praca renderowania po zmianie języka: Bardziej szczegółowa analiza: ile wysiłku kosztowało interfejs ponowne odrysowanie dla nowego języka po rozpoczęciu zmiany. Przydatne, gdy „odczuwalny” czas i koszt frameworka się różnią.
Początkowy czas ładowania strony: Od nawigacji do momentu, w którym przeglądarka uzna stronę za w pełni załadowaną dla testowanych przeze mnie scenariuszy. Dobre do porównywania „zimnych startów” (cold starts).
Czas hydratacji (Hydration): Gdy aplikacja to udostępnia, ile czasu klient spędza na zamianie kodu HTML z serwera w coś, co można faktycznie kliknąć. Myślnik w tabelach oznacza, że dana implementacja nie dostarczyła wiarygodnej wartości hydratacji w tym benchmarku.
Gwiazdki na GitHubie
Gwiazdki na GitHubie są silnym wskaźnikiem popularności projektu, zaufania społeczności i długoterminowego znaczenia. Choć nie są bezpośrednią miarą jakości technicznej, odzwierciedlają, ilu programistów uważa projekt za przydatny, śledzi jego postępy i prawdopodobnie go przyjmie. Przy szacowaniu wartości projektu gwiazdki pomagają porównać zainteresowanie alternatywami i dostarczają wglądu w rozwój ekosystemu.
Szczegóły wyników
1 - Rozwiązania, których należy unikać
Należy wyraźnie unikać niektórych rozwiązań, takich jak gt-next czy lingo.dev. Łączą one uzależnienie od dostawcy (vendor lock-in) z zaśmiecaniem bazy kodu. Mimo wielu godzin spędzonych na próbach ich wdrożenia, nigdy nie udało mi się sprawić, by działały poprawnie - ani w TanStack Start, ani w Next.js.
Napotkane problemy:
(General Translation) ([email protected]):
- W przypadku aplikacji o rozmiarze 110 KB,
gt-nextdodaje ponad 440 KB ekstra. - Komunikat
Quota Exceeded, please upgrade your planprzy pierwszej próbie budowania z General Translation. - Tłumaczenia nie są renderowane; otrzymuję błąd
Error: <T> used on the client-side outside of <GTProvider>, co wydaje się być błędem w bibliotece. - Podczas wdrażania gt-next napotkałem również problem z biblioteką: błąd
does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser, który powodował awarię aplikacji. Po zgłoszeniu tego problemu opiekun naprawił go w ciągu 24 godzin. - Biblioteka blokuje statyczne renderowanie stron Next.js.
(Lingo.dev) (@lingo.dev/[email protected]):
- Przekroczenie limitu AI, co całkowicie blokuje budowanie - nie można zatem wdrożyć aplikacji na produkcję bez płacenia.
- Kompilator pomijał niemal 40% przetłumaczonej treści. Musiałem przepisać wszystkie struktury
.mapna płaskie bloki komponentów, aby go uruchomić. - Ich CLI jest zabugowane i miało tendencję do resetowania pliku konfiguracyjnego bez powodu.
- Podczas budowania całkowicie wymazywało wygenerowane pliki JSON, gdy dodawana była nowa treść. W rezultacie kilka kluczy mogło wymazać ponad 300 istniejących kluczy.
2 - Rozwiązania eksperymentalne
(Wuchale) ([email protected]):
Idea stojąca za Wuchale jest interesująca, ale nie jest to jeszcze opłacalne rozwiązanie. Napotkałem problemy z reaktywnością i musiałem wymusić ponowne renderowanie dostawcy, aby aplikacja działała. Dokumentacja jest również dość niejasna, co utrudnia wdrożenie.
(Paraglide) (@inlang/[email protected]):
Paraglide oferuje innowacyjne i przemyślane podejście. Mimo to w tym benchmarku reklamowany tree-shaking nie zadziałał w moich konfiguracjach Next.js ani TanStack Start. Przepływ pracy i DX są bardziej złożone niż w innych opcjach.
Osobiście nie podoba mi się konieczność regenerowania plików JS przed każdym przesłaniem kodu (push), co stwarza ciągłe ryzyko konfliktów scalania (merge conflicts) przy Pull Requestach. Narzędzie wydaje się również bardziej skoncentrowane na Vite niż na Next.js.
Wreszcie, w porównaniu z innymi rozwiązaniami, Paraglide nie używa magazynu (np. React context) do pobierania bieżącej lokalizacji w celu renderowania treści. Dla każdego przeanalizowanego węzła żąda lokalizacji z localStorage / cookie itp. Prowadzi to do wykonywania niepotrzebnej logiki, co wpływa na reaktywność komponentu.
Notatka o paraglide: rozwiązanie to wstrzykuje kod do Twojej bazy kodu w celu importu, w wyniku czego metryka 'rozmiar biblioteki' w raporcie benchmarka wynosi prawie 0. Generowanie kodu to dobra rzecz, ponieważ używana funkcja będzie zawierać tylko niezbędną logikę (wszystkie prefiksy vs brak prefiksów, ciasteczka vs storage itp.). Dla porównania, Intlayer wykonuje to filtrowanie poprzez wstrzykiwanie zmiennych środowiskowych w procesie budowania, aby wymusić na bundlerze usunięcie treści (tree-shake) w zależności od logiki. Dzięki temu paraglide i intlayer okazują się być od 6 do 10 razy lżejszymi rozwiązaniami niż i18next czy next-intl.
3 - Akceptowalne rozwiązania
(Tolgee) (@tolgee/[email protected]):
Tolgee rozwiązuje wiele z wymienionych wcześniej problemów. Uznałem jednak, że trudniej go wdrożyć niż podobne narzędzia. Nie zapewnia on bezpieczeństwa typów (type safety), co utrudnia również wyłapywanie brakujących kluczy w czasie kompilacji. Musiałem opakować funkcje Tolgee we własne funkcje, aby dodać wykrywanie brakujących kluczy.
(Next Intl) ([email protected]):
next-intl to obecnie najmodniejsza opcja i ta, którą agenci AI forsują najbardziej, ale moim zdaniem niesłusznie. Rozpoczęcie pracy jest łatwe. W praktyce optymalizacja pod kątem ograniczania wycieków jest złożona. Połączenie dynamicznego ładowania + przestrzeni nazw + typów TypeScript bardzo spowalnia rozwój. Pakiet jest również dość ciężki (~13 KB dla NextIntlClientProvider + useTranslations, co stanowi ponad 2-krotność next-intlayer). next-intl blokował niegdyś statyczne renderowanie stron Next.js. Udostępnia on funkcję pomocniczą o nazwie setRequestLocale(). Wydaje się, że zostało to częściowo rozwiązane w przypadku scentralizowanych plików, takich jak en.json / fr.json, ale statyczne renderowanie wciąż przestaje działać, gdy treść jest podzielona na przestrzenie nazw, takie jak en/shared.json / fr/shared.json / es/shared.json.
(Next I18next) ([email protected]):
next-i18next jest prawdopodobnie najpopularniejszą opcją, ponieważ był jednym z pierwszych rozwiązań i18n dla aplikacji JavaScript. Posiada on wiele wtyczek społecznościowych. Ma on te same główne wady co next-intl. Pakiet jest szczególnie ciężki (~18 KB dla I18nProvider + useTranslation, około 3-krotność next-intlayer).
Różnią się również formaty komunikatów: next-intl używa ICU MessageFormat, podczas gdy i18next używa własnego formatu.
(Next International) ([email protected]):
next-international również radzi sobie z powyższymi problemami, ale nie różni się zbytnio od next-intl czy next-i18next. Zawiera on funkcję scopedT() dla tłumaczeń specyficznych dla przestrzeni nazw, ale jej użycie nie ma praktycznie żadnego wpływu na rozmiar pakietu.
(Lingui) (@lingui/[email protected]):
Lingui jest często chwalony. Osobiście uznałem, że proces lingui extract / lingui compile jest bardziej złożony niż w przypadku alternatyw, bez wyraźnej przewagi. Zauważyłem również niespójne składnie, które mylą AI (np. t(), t'', i18n.t(), <Trans>).
4 - Rekomendacje
(Next Translate) ([email protected]):
next-translate to moja główna rekomendacja, jeśli lubisz API w stylu t(). Działa on elegancko poprzez next-translate-plugin, ładując przestrzenie nazw przez getStaticProps za pomocą loadera Webpack / Turbopack. Jest to również najlżejsza opcja w tym zestawieniu (~2.5 KB). Jeśli chodzi o przestrzenie nazw, definiowanie ich na poziomie strony lub trasy w konfiguracji jest przemyślane i łatwiejsze w utrzymaniu niż w przypadku głównych alternatyw, takich jak next-intl czy next-i18next. W wersji 3.1.2 zauważyłem, że statyczne renderowanie nie działało; Next.js powracał do renderowania dynamicznego.
(Intlayer) ([email protected]):
Nie będę osobiście oceniał next-intlayer ze względu na obiektywizm, ponieważ jest to moje własne rozwiązanie.
Notatka osobista
Ta notatka jest osobista i nie wpływa na wyniki benchmarku. W świecie i18n często widać konsensus wokół wzorca const t = useTranslation('xx') + <>{t('xx.xx')}</>.
W aplikacjach React wstrzykiwanie funkcji jako ReactNode jest w mojej ocenie anty-wzorcem. Dodaje to również zbędną złożoność i narzut wykonania JavaScript (nawet jeśli prawie niezauważalny).