Autor:
    Erstellung:2026-09-13Letzte Aktualisierung:2026-09-13

    next-intl VS @intlayer/next-intl | Gleiche API, Unterschiedliches Bundle

    @intlayer/next-intl ist ein Compat-Adapter: Er stellt die next-intl API (useTranslations, getTranslations, useLocale, t.rich(), ICU Plurals, NextIntlClientProvider...) bereit und serviert diese aus von Intlayer kompilierten Dictionaries. Der Application Code ändert sich nicht. Das Bundle schon.

    Dieser Artikel vergleicht die beiden auf derselben Next.js-Anwendung, einmal gebaut mit next-intl und einmal mit dem Adapter. Die Zahlen stammen von Benchmark Bloom, einer Open-Source-Suite, die aufzeichnet, was der Browser tatsächlich herunterlädt. Wenn Sie den next-intl vs Intlayer Vergleich als Bibliotheken möchten, lesen Sie next-intl vs Intlayer. Dieser Artikel handelt davon, was der Adapter ändert, wenn Sie Ihre Komponenten unverändert lassen.

    tl;dr: Bei derselben Next.js-App reduzierte der Austausch von next-intl gegen @intlayer/next-intl das JavaScript pro Seite von 153,6 KB auf 147,5 KB gzip, die durchschnittliche Komponente von 21,8 KB auf 8,1 KB, Zeichenlecks fremder Seiten von ~90% auf 0% und Hydration von 14,7 ms auf 12,8 ms, ohne eine Komponente zu bearbeiten. Bei TanStack Start reduzierte das Äquivalent use-intl (@intlayer/use-intl) Komponenten von 76-87 KB auf 9-11 KB und Locale-Wechsel von 7-21 ms auf 4-9 ms. Der Adapter kostet 8,0 KB Runtime gegenüber 14,7 KB für next-intl und 5,5 KB für natives next-intlayer. Navigation und Middleware werden auf Intlayers Routing-Konfiguration neu implementiert; lokalisierte pathnames sind die einzige Funktion, die nicht übernommen wird.

    Was @intlayer/next-intl ist

    next-intl ist eine Runtime: getRequestConfig lädt eine messages/{locale}.json pro Request, NextIntlClientProvider sendet sie an den Client, und useTranslations("about") liest zur Render-Zeit Schlüssel aus diesem Objekt. Jede Optimierung (Namespaces, pick(messages, [...]) pro Seite, Lazy Loading) musst du selbst schreiben.

    @intlayer/next-intl behält den ersten und letzten Teil dieser Kette bei und ersetzt den mittleren. Deine Komponenten rufen weiterhin useTranslations("about") auf; was sie erhalten, kommt aus einem zur Build-Zeit kompilierten Intlayer Dictionary, auf die jeweilige Komponente begrenzt, nur in der aktiven Sprache.

    Drei Mechanismen machen das möglich:

    1. Import-Aliasing. createNextIntlPlugin() aus @intlayer/next-intl/plugin umhüllt withIntlayer und fügt Webpack / Turbopack-Aliase hinzu, damit next-intl, next-intl/server, next-intl/navigation und next-intl/middleware zu @intlayer/next-intl aufgelöst werden. Kein Import in deiner Codebase wird umbenannt.
    2. JSON als Quelle der Wahrheit. Das syncJSON-Plugin liest deine vorhandenen messages/{locale}.json, teilt ihre Top-Level-Keys in ein Dictionary pro Namespace auf und schreibt Übersetzungen in dieselben Dateien zurück, wenn die CLI oder das CMS diese aktualisiert. Der Workflow deiner Übersetzer bleibt unverändert.
    3. Call-site binding. Der Intlayer-Optimierungspass (Babel oder SWC) schreibt useTranslations("about") in einen Aufruf um, der das about-Wörterbuch direkt empfängt. Die Komponente greift nicht mehr auf einen globalen Message-Tree zu; sie greift auf ihren eigenen Content zu.
    app/[locale]/about/page.tsx
    // Ihr Code, unverändert
    import { useTranslations } from "next-intl";
    
    const AboutPage = () => {
      const t = useTranslations("about");
      return <h1>{t("title")}</h1>;
    };
    
    Was der Compiler ausgibt (vereinfacht)
    import _dicHash_about from "../.intlayer/dictionaries/about.mjs";
    import { useDictionary as useTranslations } from "@intlayer/next-intl";
    
    const AboutPage = () => {
      const t = useTranslations(_dicHash_about);
      return <h1>{t("title")}</h1>;
    };
    

    Dieses Rewrite ist der Grund, warum die Spalten "component-size" und "page-leakage" unten verschoben werden: Eine Seite lädt nur die Dictionaries der Komponenten, die sie rendert, und nur in dem Locale, das bereitgestellt wird.

    Was der Adapter behält, ignoriert und nicht ersetzt

    next-intl APIMit @intlayer/next-intl
    useTranslations("ns") / getTranslations("ns")✅ Beibehalten. An das ns Dictionary zur Compile-Zeit gebunden. Schlüssel sind typisiert gegen Ihren Content.
    getTranslations({ locale, namespace })✅ Beibehalten
    t("key", { name }), t.rich(), t.markup(), t.raw()✅ Beibehalten. ICU plurals, select, selectordinal, #, {ts, date, long} werden durch Intlayers ICU-Resolver verarbeitet
    useLocale() / getLocale() / setRequestLocale() / setLocale✅ Beibehalten
    useFormatter()✅ Beibehalten. dateTime, number, relativeTime, list, dateTimeRange werden zu nativem Intl weitergeleitet
    NextIntlClientProvider✅ Beibehalten. Die Props messages, timeZone und now werden akzeptiert, aber ignoriert (eine Entwicklerwarnung informiert Sie darüber)
    getMessages()✅ Beibehalten für Kompatibilität; nicht mehr erforderlich
    getRequestConfig() in src/i18n.ts⚠️ Nicht erforderlich. Wörterbücher werden zur Build-Zeit kompiliert; es gibt kein Laden von Pro-Request-Nachrichten
    defineRouting()✅ Beibehalten. Ausgelassene Felder (locales, defaultLocale, localePrefix) werden aus intlayer.config.ts gelesen
    createNavigation(), Link, redirect, usePathname, useRouter✅ Beibehalten. Neu implementiert auf Intlayer's Routing-Konfiguration; das routing-Argument wird akzeptiert, aber ignoriert
    pathnames (lokalisierte Routennamen)❌ Für Typisierung akzeptiert, nicht interpoliert. Behalten Sie einfache Pfadnamen oder verschieben Sie diese Zuordnung zu Intlayer's rewrite
    createMiddleware()✅ Beibehalten. Gibt Intlayer's Proxy zurück; setzt das NEXT_LOCALE-Cookie, damit useLocale() und Ihr Switcher weiterhin funktionieren
    NEXT_LOCALE Cookie✅ Standardmäßig gelesen (es sei denn, Sie konfigurieren routing.storage selbst)
    Bare useTranslations() ohne Namespace⚠️ Funktioniert, aber die Aufrufstelle ist nicht gebunden: sie wird durch die Runtime-Registry aufgelöst. Übergeben Sie einen Namespace, um die Bundle-Gewinne zu erhalten

    Der Benchmark

    Was wurde gemessen

    Die Benchmark Bloom Suite erstellt dieselbe Anwendung mit jedem Setup: 10 Seiten (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 Locales (en, fr, es, de, it, pt, zh, ja, ko, ru), identische Komponenten und identischer Inhalt. Seiten werden in en und fr gemessen.

    next-intl wurde mit vier Ladestrategien entwickelt, von der naiven Einrichtung (messages/{locale}.json vollständig geladen) bis zur optimalen Lösung (ein Namespace pro Route + pro-Seite pick()). Der Adapter wurde auf denselben Komponenten wie die naive Einrichtung entwickelt, wobei nur next.config.ts und intlayer.config.ts geändert wurden. Er hat keine "scoped"-Variante: Der Compiler scoped den Inhalt pro Komponente, daher sind seine static- und dynamic-Zeilen bereits gescoped.

    Für jeden Build zeichnet die Suite auf:

    • Lib size: gzip-Größe einer leeren Komponente, die nur die i18n-Bibliothek importiert. Die fixen Kosten der Runtime.
    • Page JS: gzip JavaScript, das pro Seite heruntergeladen wird, gemittelt über alle Seiten und Locales.
    • Locale leak %: Anteil der übersetzten Strings im heruntergeladenen JS, die zu einem Locale gehören, das der Benutzer nicht anzeigt.
    • Page leak %: Anteil der übersetzten Strings im heruntergeladenen JS, die zu einer Seite gehören, auf der sich der Benutzer nicht befindet.
    • Component avg: durchschnittliche gzip-Größe jeder Komponente, die isoliert kompiliert wird. Zeigt, wie viel i18n-Runtime und Katalog eine einzelne Komponente mit sich bringt.
    • E2E reactivity: Wanduhrzeit zwischen der Auswahl eines neuen Locales und dem Aktualisieren von html[lang] im DOM (Playwright, 5 Iterationen).
    • Hydration: Dauer der React-Hydration-Phase.
    Die nachfolgenden Zahlen stammen aus dem Durchlauf vom 2026-09-12 mit next-intl / use-intl 4.14.2 und @intlayer/* 9.5.1. Die Test-Anwendung ist absichtlich klein (einige Dutzend Strings pro Locale), sodass die Leak-Prozentsätze ein Muster beschreiben: Sie wachsen mit Ihrem Inhalt, während die Runtime-Kosten konstant bleiben.

    Ergebnisse auf Next.js

    SetupStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (no i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    @intlayer/next-intlstatic8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intldynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms
    next-intlayer (native)static5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayer (native)dynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms

    Wie man es liest

    • Gleiche Komponenten, 6 KB weniger pro Seite. Der Adapter-Build der naiven App landet bei 147.5 KB, unter jeder next-intl-Konfiguration, einschließlich der vollständig optimierten (153.6 KB). Die Runtime selbst ist der Unterschied: 8.0 KB gegenüber 14.7 KB, auf jeder Seite zu zahlen.
    • Lecks gehen auf 0% ohne Änderung einer Komponente. Das naive next-intl-Setup versendet ~90% von fremdsprachigen Seiten-Strings auf jeder Seite. Um 0% mit next-intl zu erreichen, sind die scoped-*-Setups erforderlich: ein Namespace pro Route und pick(messages, [...]) auf jeder Seite. Der Adapter erreicht 0% aus dem naiven Code, weil der Optimize-Pass jeden useTranslations("ns") an sein eigenes Dictionary bindet.
    • Komponenten schrumpfen um das 2,7-fache. Eine isoliert kompilierte Komponente belegt durchschnittlich 21,8 KB mit next-intl (sie erreicht den Provider und den Message-Tree) und 8,1 KB mit dem Adapter. Im scoped-static-Setup von next-intl geht diese Zahl auf 80 KB, weil jede Route's Namespace-Datei von der Seite aus erreichbar wird, die sie auswählt.
    • Hydration ist 2 ms schneller (12,8 vs 14,7 ms): Es gibt kein Message-Objekt, das vor der React-Hydration aus der RSC-Payload deserialisiert werden muss.
    • Der Adapter ist nicht die native Runtime. next-intlayer liegt bei 141,3 KB, +0,3 KB über der Base-App, mit einer 5,5 KB Runtime. Der Adapter bietet die next-intl API-Oberfläche (useFormatter, t.rich, der ICU-Resolver) auf top von Intlayers Core, daher 8,0 KB und +6 KB pro Seite. Es ist die Brücke, nicht das Ziel.

    Ergebnisse auf TanStack Start (use-intl)

    use-intl ist der Framework-agnostische Core von next-intl. Sein Adapter, @intlayer/use-intl, folgt dem gleichen Design mit einem Vite-Plugin (@intlayer/use-intl/plugin).

    SetupStrategieLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (no i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms15.3 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms15.4 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms24.8 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms25.9 ms
    @intlayer/use-intlstatic7.3 KB135.8 KB49.7%0.0%10.9 KB4.2 ms10.5 ms
    @intlayer/use-intldynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms16.1 ms
    intlayer (native)static5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayer (native)dynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms

    Wie man es liest

    • Pro-Seite Bytes sind ein Unentschieden gegen das optimierte use-intl. @intlayer/use-intl im dynamic Modus (129.7 KB) liegt innerhalb von 1 KB von use-intl's scoped-dynamic (128.7 KB) und 10 KB über use-intl's einfachem dynamic (119.4 KB). Diese einfache dynamic Zeile leckt immer noch 90% der Strings von Fremdseiten; die Bytegröße ist niedrig, weil der Inhalt der Test-App klein ist. Der 0% Wert des Adapters bleibt flach, wenn der Inhalt wächst.
    • Komponenten sind 7-9x kleiner. use-intl Komponenten sind im Durchschnitt 76-87 KB in jeder Strategie, weil useTranslations an das gesamte Message-Objekt des Providers gebunden ist. Der Adapter hat durchschnittlich 9-11 KB.
    • Locale-Wechsel ist schneller. Die optimierten use-intl Setups benötigen 13-21 ms um html[lang] zu aktualisieren; der Adapter benötigt 4-9 ms. Weniger Komponenten werden neu gerendert, und nichts wird aus einem Message-Baum neu ausgewählt.
    • static behält jedes Locale. Die static Zeile des Adapters zeigt 49,7% Locale-Leckage, dasselbe wie natives Intlayer im static Modus: alle Locales werden gebündelt, nur die Dictionaries der Seite. Eine Konfigurationszeile (importMode: 'dynamic') entfernt es.

    Warum sich die Zahlen ändern

    Nichts in der Komponente hat sich geändert, daher stammen die Verbesserungen vollständig davon, woran useTranslations gebunden ist.

    Mit next-intl ist die Bindung der Provider. NextIntlClientProvider erhält das gesamte messages-Objekt für das Locale; jeder useTranslations("about")-Aufruf liest daraus. Der Bundler sieht eine Komponente, die einen Hook importiert, der einen Context liest, und kann nicht wissen, dass nur der about-Zweig verwendet wird. Die Routen unten teilen sich alle das gleiche Message-Objekt, daher zeigt die Spalte page-leak ~90%, bis du die Datei selbst aufteilst.

    bash
    .
    ├── messages
       ├── en.json                       # jeder Namespace, jede Seite
       └── fr.json
    └── src
        ├── i18n.ts                       # getRequestConfig({ messages: await import(...) })
        ├── middleware.ts                 # createMiddleware(routing)
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider messages={messages}>
            └── about/page.tsx            # useTranslations("about")
    

    Mit @intlayer/next-intl ist die Bindung das Dictionary. syncJSON wandelt messages/en.json in ein Dictionary pro Top-Level-Key um; der Compiler löst auf, welche Komponente useTranslations("about") aufruft, und übergibt ihr about direkt, in der aktiven Sprache, als Import, den der Bundler nachverfolgen und aufteilen kann.

    bash
    .
    ├── intlayer.config.ts                # syncJSON({ source: ({ locale }) => `./messages/${locale}.json` })
    ├── messages
       ├── en.json                       # unverändert, immer noch die Quelle der Wahrheit
       └── fr.json
    ├── .intlayer/                        # generiert: ein Dictionary pro Namespace, pro Locale
    └── src
        ├── middleware.ts                 # createMiddleware() gibt nun Intlayers Proxy zurück
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider> (keine messages prop)
            └── about/page.tsx            # useTranslations("about")  ← unverändert
    

    src/i18n.ts und die messages prop entfallen. Alles andere bleibt identisch.

    Migration in drei Schritten

    1. Installation

      bash
      npx intlayer init --interactive
      

      Der Befehl erkennt next-intl und installiert intlayer, next-intlayer, @intlayer/next-intl und @intlayer/sync-json-plugin. Behalten Sie next-intl installiert: Es ist eine Peer-Abhängigkeit des Adapters und stellt die Typen bereit.

    2. Intlayer auf deine Messages hinweisen

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      import { syncJSON } from "@intlayer/sync-json-plugin";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
          defaultLocale: Locales.ENGLISH,
        },
        dictionary: {
          // "static" bündelt jedes Locale; "dynamic" lädt das aktive on Demand
          importMode: "dynamic",
        },
        plugins: [
          syncJSON({
            // ICU Platzhalter: {name}, {count, plural, one {# item} other {# items}}
            format: "icu",
            source: ({ locale }) => `./messages/${locale}.json`,
            location: "messages",
          }),
        ],
      };
      
      export default config;
      

      messages/{locale}.json bleibt an seinem Platz. Jeder Top-Level-Schlüssel wird zu einem Dictionary; useTranslations("about") wird dem about Dictionary zugeordnet.

    3. next.config.ts umhüllen

      next.config.ts
      import type { NextConfig } from "next";
      import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
      
      const withIntlayer = createNextIntlPlugin();
      
      const nextConfig: NextConfig = {};
      
      export default withIntlayer(nextConfig);
      

      createNextIntlPlugin() setzt withIntlayer zusammen (Content Watching, Dictionary Compilation, der Optimize Pass) und die next-intl@intlayer/next-intl Aliases für Webpack und Turbopack. Bauen Sie, und die Zahlen in den obigen Tabellen sind Ihre.

    Was Sie danach löschen können

    Datei / MusterGrund
    getRequestConfig in src/i18n.tsKein per-Request Message Loading. Behalten Sie die Datei nur, wenn sie auch createNavigation Helfer exportiert
    messages={...} auf NextIntlClientProviderDer Adapter liest die kompilierte Ausgabe; das Prop wird ignoriert und protokolliert eine Warnung in der Entwicklung
    await getMessages() in LayoutsGleicher Grund
    Pro-Seite pick(messages, [...])Der Compiler führt das Picking pro Komponente durch

    Was du darüber hinaus gewinnst

    • Typisierte Keys. useTranslations("about") ist gegen das kompilierte about-Dictionary typisiert. t("does.not.exist") ist ein TypeScript-Fehler, kein Runtime-Fallback.
    • npx intlayer test schlägt fehl in CI, wenn einem Locale ein Schlüssel fehlt. npx intlayer fill übersetzt die fehlenden Schlüssel mit dem Anbieter Ihrer Wahl (OpenAI, Anthropic, Mistral, Gemini...) unter Verwendung Ihres eigenen Schlüssels und schreibt das Ergebnis zurück in messages/{locale}.json.
    • Visual Editor und CMS arbeiten mit denselben Dictionaries, sodass Nicht-Entwickler messages/fr.json durch eine Benutzeroberfläche bearbeiten können und die Datei aktualisiert wird.
    • Schrittweise Migration zu .content.ts. Jede Komponente kann von useTranslations("about") zu useIntlayer("about") mit einer Co-located Content-Datei wechseln, eins nach dem anderen. JSON- und .content.ts-Dictionaries koexistieren und werden zusammengeführt.

    Zu beachtende Limits vor dem Start

    • Routing-Konfiguration verschiebt sich zu intlayer.config.ts. createNavigation(routing) und createMiddleware(routing) behalten ihre Signatur, ignorieren aber das Argument: Locales, Default-Locale und Präfix-Strategie kommen aus Intlayers routing-Konfiguration. Wenn du next-intl's lokalisierte pathnames verwendest (/about/a-propos), interpoliert der Adapter diese nicht; Intlayers routing.rewrite deckt diesen Fall ab, aber es ist eine separate Änderung.
    • Namespace-loses useTranslations() ist nicht gebunden. Der Optimize-Pass benötigt einen statischen Namespace, um zu wissen, welches Dictionary importiert werden soll. Ein bloßer Aufruf funktioniert immer noch über eine Runtime-Registry, die auf jedes Dictionary verweist, was genau das Leck ist, das du entfernen wolltest. Übergib den Namespace.
    • Der Adapter ist nicht kostenlos. 8,0 KB Runtime gegenüber 5,5 KB für next-intlayer, und +6-7 KB pro Seite gegenüber dem nativen Build. Dies zahlt sich durch die next-intl API-Oberfläche aus. Wenn Sie an den Punkt gelangen, an dem jede Komponente zu useIntlayer verschoben wurde, lassen Sie den Adapter fallen.
    • messages, timeZone, now auf dem Provider werden ignoriert. Die Formatter werden durch natives Intl gestützt und nur das Locale beeinflusst deren Ausgabe; wenn Sie auf eine erzwungene Zeitzone oder ein fixes now für Hydrations-stabile Daten angewiesen sind, handhaben Sie dies am Aufrufort.

    Wann sollte man welche verwenden?

    • Bleiben Sie bei next-intl, wenn Ihre App klein ist, Ihr Bundle kein Problem ist, und Ihr Team sich damit wohlfühlt, Namespaces und pick() pro Seite zu verwalten.
    • Nutzen Sie @intlayer/next-intl, wenn Sie derzeit next-intl verwenden und von Bundlegröße, weniger Leakage, Hydration-Verbesserungen, typisierten Keys und den CLI-/CMS-Tools ohne vollständiges Rewrite profitieren möchten. Dies ist der empfohlene Einstiegspunkt für bestehende next-intl-Codebasen.
    • Wechseln Sie zu nativen Lösungen (next-intlayer) für neue Projekte oder sobald der Adapter seinen Zweck erfüllt hat. Es ist die leichteste der drei Varianten (5,5 KB, +0,3 KB pro Seite) und ermöglicht synchrone Server Components, per-Component .content.ts-Dateien und den vollständigen Feature-Set.

    Verwandte Vergleiche

    Fazit

    @intlayer/next-intl macht eine Sache: Es ändert, woran useTranslations gebunden ist, von einem Provider, der jede Nachricht enthält, zu einem für diese Komponente kompilierten Dictionary. In derselben Next.js-App, die 6 KB pro Seite wert ist, 2,7x kleinere Komponenten, 0% Lecks und 2 ms Hydration, bevor jemand eine Komponentendatei öffnet. Navigation und Middleware behalten ihre API auf Intlayers Routing-Konfiguration, und die native next-intlayer-Runtime bleibt noch leichter.

    Alle Rohdaten, die Test-Apps und die Scripts befinden sich im Benchmark Bloom Repository. Führen Sie es selbst aus.

    Weitere Details finden Sie in der Dokumentation "Why Intlayer?".

    Kommentare

    Noch keine Kommentare. Seien Sie der Erste, der seine Gedanken teilt.

    Ähnliche Beiträge

    Letzte Beiträge