Dec 16, 2025

Architecture for Next.js App Router i18n at Scale: Fixing 100+ Locale SSR Bottlenecks

Struggling with slow SSR, bloated bundles, and cache explosions in Next.js i18n? Discover a proven App Router architecture for 100+ locales.

Author

Mehar MiddhaMehar MiddhaSoftware Engineer - II
Architecture for Next.js App Router i18n at Scale: Fixing 100+ Locale SSR Bottlenecks

If you have ever worked on a global product with 50, 70, or even 100+ locales, you already know the dark side of internationalization:

  • Slow SSR rendering
  • Bloated server bundles
  • 20–50 MB of locale JSONs shipped to the client
  • Random hydration mismatches
  • Route cache exploding due to per-locale permutations

This blog addresses the modern problem that most tutorials overlook: Next.js 13/14, Server Components, and i18n at scale. And more importantly, how to fix it.

The Problem: Your SSR Pages Become Sluggish as Locale Count Grows

Let’s say your next-gen product supports:

  • 108 locales
  • ~20 JSON namespaces per locale
  • Large translation files (500–2,000 keys each)

This is common for apps that operate in the Middle East, Europe, and Asia.

But Suddenly:

  1. SSR time increases from 60ms → 600ms, because loading translations becomes synchronous and heavy.
  2. Your server bundles balloon, since all locales. JSON files get bundled by Webpack.
  3. Switching locale makes the whole page re-render on the server. Each locale triggers a unique cached version of every SSR fetch. 
  4. Static builds become painfully slow. Next.js attempts to prebuild pages for each locale. 
  5. Memory usage skyrockets. Your server loads hundreds of megabytes of translation JSON.

But why does this happen in Next.js 13/14 (With App Router)

The default implementation patterns clash with the core concepts of the App Router. There are three hidden bottlenecks:

1. JSON Locales Included in the Server Bundle

If you import like this:

// DON'T DO THIS AT SCALE! 
import en from './locales/en/common.json';
import fr from './locales/en/common.json';
// ... 100+ static imports 
// Result: 100 locales × 20 files × 20 KB = 40 MB server bundle 

This default behavior is intended to make all imported code and data reliably available; however, at scale, it becomes a disaster for cold start and bundle size.

2. Server Components Load Entire Locale Namespaces Synchronously

Traditional pattern:

const dict = await getDictionary(locale);
return <Layout dict={dict} />;

getDictionary() loads ALL namespaces needed for the entire tree.

Even if only 15% of those keys are used on that route.

3. Each Locale Causes Unique Caching Layers

fetch() in server components is cached automatically.

So each locale creates:

  • a unique segment cache
  • unique RSC payload
  • unique route response

Meaning:

 "Page × Locale Count" = cache size.

THE SOLUTION: A Fully Optimized i18n Architecture for Next.js SSR

This approach is battle-tested for apps with 100+ locales. We break the solution into 5 powerful optimization layers:

1. Lazy Load Translations per Namespace (NOT per locale)

Do not import JSON statically. Instead, dynamically import based on both locale and namespace:

export async function getDictionary(locale: string, ns: string) {
  return (await import(`./locales/${locale}/${ns}.json`)).default;
}

This gives you:

  • No more bundling all JSON files
  • Load only what the page needs
  • Faster cold starts on the server

2. Load Translations Inside Server Components (Not Layouts)

Most tutorials load translations in <Layout> — wrong for large apps.

Instead, load where needed:

export default async function Page({ params: { locale } }) {
  const dict = await getDictionary(locale, "common");
  return <Home dict={dict} />;
}

Why?

  • Layouts wrap 1000s of components
  • The layout cache becomes huge
  • Changing locale invalidates the whole app

Load only per-page translations, not global ones.

3. Split Large Locale Files Into Smaller Namespaces

Instead of:

locales/en.json
locales/fr.json

Use structured namespaces:

locales/en/common.json
locales/en/auth.json
locales/en/errors.json
locales/en/homepage.json

Advantages:

  • Load only 1–2 namespaces per page
  • Smaller JSON network cost
  • Lower memory footprint

4. Use Edge Caching + RSC Cache to Reduce Locale Re-renders

Next.js automatically caches server component output.

But with 100 locales, the cache grows too large.

Solution: custom caching with small TTL:

Custom dictionary caching implementation for Next.js i18n performance

Benefits:

  • Faster SSR
  • Avoid loading JSON repeatedly
  • Prevent memory leaks

5. Stream Translations Instead of Preloading (Real Big-App Trick)

This is an advanced RSC technique:

Use Suspense boundaries to stream translations:

export default function Page({ params }) {
  return (
    <Suspense fallback={<LoadingTranslations />}>
      <Translations locale={params.locale} />
    </Suspense>
  );
}

async function Translations({ locale }) {
  const dict = await getDictionary(locale, "homepage");
  return <Home dict={dict} />;
}

Impact:

  • Server responds immediately
  • Translations inserted as they load
  • Faster TTFB
  • Great for SEO

Debugging Performance Gains

Here are realistic improvements from apps with 100+ locales:

Area

Before

After

SSR Time

450–800 ms

80–150 ms

Server Bundle

30–50 MB

5–8 MB

Memory Usage

700–900 MB

200–350 MB

Locale Switch Time

300–600 ms

100–200 ms

Cold Start

1–2 s

200–300 ms

Conclusion: The Golden Rule of i18n in Large Next.js Apps

Your app should only load the translations needed by the component currently rendering. Nothing else.

When you scale to 50+ locales:

  • The bundle size becomes the bottleneck
  • SSR becomes the bottleneck
  • Caching becomes unpredictable

The only solution is to load less, not “optimize more”. This architecture does exactly that.

Subscribe to Our Newsletter

RELATED ARTICLES

More from the engineering frontline.

Dive deep into our research and insights on design, development, and the impact of various trends to businesses.
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
A real-world look at how oversized images can trigger Safari reloads and iOS crashes in Flutter apps and how smarter image decoding prevents them.
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
This blog explores how Flutter Agent Skills improve AI-assisted development by combining official framework workflows with project-specific guidance for more consistent development.
Why Everything Your AI Builds Looks the Same
Why Everything Your AI Builds Looks the Same
This blog explores why AI-generated interfaces often look alike and explains how design systems, product context, and reusable engineering practices help teams build distinctive, scalable
How We Built the Missing Bridge from Code to Figma
Technology

Jul 10, 2026

How We Built the Missing Bridge from Code to Figma
This blog explores how AI-generated React apps get turned into fully editable, designer-ready Figma files by reading React Fiber instead of the DOM.
Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
Technology

Jun 27, 2026

Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
A practical breakdown of building resilient AWS-to-on-premises connectivity with WireGuard HA, active-standby failover, and deep packet-forwarding observability.
We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
Technology

Jun 19, 2026

We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
A practical guide to building a 114-second multi-cloud disaster recovery failover between AWS and Azure — what we built, what broke, and what we learned.
Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
Technology

Jun 12, 2026

Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
This blog explains how organizations can balance speed, scalability, and operational flexibility as they grow from startup to enterprise scale.

The Right Conversation Can Save You Six Months.

Whether you’re navigating AI adoption, modernizing legacy systems, or scaling a product - we start by listening. No pitch deck. No template. A real conversation.