PatternsInternationalization
Lazy Loading Translations
Optimize bundle size by code splitting translation files
Lazy Loading Translations
Loading all translations upfront bloats your bundle. Lazy loading translations by route or feature reduces initial load time and improves performance.
The Problem
// ❌ BAD: Load all translations
import en from './messages/en.json'; // 500KB
import pt from './messages/pt.json'; // 500KB
import es from './messages/es.json'; // 500KB
import ar from './messages/ar.json'; // 500KB
import fr from './messages/fr.json'; // 500KB
// Total: 2.5MB loaded upfront!Strategies
1. Split by Route
messages/
├── en/
│ ├── common.json # Always loaded
│ ├── home.json # Load on homepage
│ ├── dashboard.json # Load on dashboard
│ ├── products.json # Load on products page
│ └── checkout.json # Load on checkout2. Split by Feature
messages/
├── en/
│ ├── common.json # Shared terms
│ ├── auth.json # Login/signup
│ ├── cart.json # Shopping cart
│ └── profile.json # User profile3. Hybrid Approach
messages/
├── en/
│ ├── core/ # Critical, always loaded
│ │ ├── common.json
│ │ ├── navigation.json
│ │ └── errors.json
│ └── features/ # Lazy loaded
│ ├── dashboard.json
│ ├── products.json
│ └── checkout.jsonImplementation (next-intl)
Basic Lazy Loading
// i18n.ts
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => {
// Only load common messages initially
const commonMessages = (await import(`./messages/${locale}/common.json`)).default;
return {
messages: commonMessages,
};
});Route-Based Loading
// app/[locale]/dashboard/page.tsx
import { getTranslations } from 'next-intl/server';
export default async function DashboardPage() {
// Load dashboard translations on-demand
const t = await getTranslations('dashboard');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('welcome')}</p>
</div>
);
}Dynamic Import in Components
'use client';
import { useState, useEffect } from 'react';
import { useLocale } from 'next-intl';
export function FeatureComponent() {
const [messages, setMessages] = useState<any>(null);
const locale = useLocale();
useEffect(() => {
// Lazy load feature translations
import(`@/messages/${locale}/feature.json`)
.then(module => setMessages(module.default))
.catch(err => console.error('Failed to load translations', err));
}, [locale]);
if (!messages) {
return <Skeleton />;
}
return <div>{messages.title}</div>;
}Webpack/Next.js Magic Comments
// Optimize chunk naming and prefetching
const loadTranslations = async (locale: string) => {
const messages = await import(
/* webpackChunkName: "translations-[request]" */
/* webpackPrefetch: true */
`./messages/${locale}/dashboard.json`
);
return messages.default;
};Preloading Strategy
Prefetch on Hover
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export function NavLink({ href, children }: Props) {
const [prefetched, setPrefetched] = useState(false);
const locale = useLocale();
const prefetchTranslations = () => {
if (prefetched) return;
// Prefetch translations for this route
const route = href.split('/').pop();
import(`@/messages/${locale}/${route}.json`)
.then(() => setPrefetched(true))
.catch(() => {});
};
return (
<a
href={href}
onMouseEnter={prefetchTranslations}
onFocus={prefetchTranslations}
>
{children}
</a>
);
}Prefetch on Viewport
'use client';
import { useEffect, useRef } from 'react';
export function PrefetchTranslations({ route }: { route: string }) {
const ref = useRef<HTMLDivElement>(null);
const locale = useLocale();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
// Prefetch when element is visible
import(`@/messages/${locale}/${route}.json`);
observer.disconnect();
}
},
{ rootMargin: '50px' }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [locale, route]);
return <div ref={ref} />;
}Cache Management
// lib/translation-cache.ts
const cache = new Map<string, any>();
export async function getTranslations(locale: string, namespace: string) {
const key = `${locale}:${namespace}`;
// Return from cache if available
if (cache.has(key)) {
return cache.get(key);
}
// Load and cache
const messages = await import(`@/messages/${locale}/${namespace}.json`);
cache.set(key, messages.default);
return messages.default;
}
// Clear cache when locale changes
export function clearTranslationCache() {
cache.clear();
}Bundle Analysis
Check Bundle Sizes
# Analyze Next.js bundle
ANALYZE=true npm run build
# Look for translation chunks
# Before: main.js (2.5MB with all translations)
# After: main.js (200KB) + dashboard.json (50KB) + products.json (80KB)Webpack Bundle Analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// ... config
});Progressive Loading
// Load translations in priority order
async function loadTranslations(locale: string) {
// 1. Critical (block render)
const common = await import(`./messages/${locale}/common.json`);
// 2. Important (show spinner)
const navigation = await import(`./messages/${locale}/navigation.json`);
// 3. Nice-to-have (load in background)
setTimeout(() => {
import(`./messages/${locale}/footer.json`);
}, 2000);
return {
...common.default,
...navigation.default,
};
}Service Worker Caching
// sw.js - Cache translations
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Cache translation files
if (url.pathname.includes('/messages/')) {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
return caches.open('translations-v1').then((cache) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
}
});Metrics
Measure Impact
// Before lazy loading
Initial Bundle: 2.5MB
Time to Interactive: 4.2s
// After lazy loading
Initial Bundle: 250KB (90% smaller!)
Time to Interactive: 1.8s (57% faster!)
Dashboard route +50KB
Products route +80KBBest Practices
- Always Load Common: Shared terms, navigation, errors
- Split by Route: Load translations when route loads
- Prefetch Smart: On hover/viewport for better UX
- Cache Aggressively: Translations don't change often
- Monitor Bundle Size: Track translation file sizes
- Lazy Load Features: Modal, drawer, tooltip translations
- Version Translations: Cache bust when updated
Common Pitfalls
❌ Loading all translations: Large initial bundle
✅ Split by route/feature
❌ No prefetching: Delay when navigating
✅ Prefetch on hover/viewport
❌ Not caching: Re-download on every route
✅ Cache in memory/SW
❌ Too granular: 100 tiny files
✅ Reasonable chunks (50-100KB)
Tools
- Next.js: Automatic code splitting
- Webpack: Magic comments for chunks
- Vite: Dynamic imports
- Rollup: Code splitting plugins
Testing
test('lazy loads translations', async () => {
const { findByText } = render(<Dashboard />);
// Initial render shows loading
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for translations to load
expect(await findByText('Dashboard Title')).toBeInTheDocument();
});Lazy loading translations is essential for performance—don't force users to download languages they won't use!