- Opublikowano
92% mniejszy bundle: jak inteligentne ładowanie tłumaczeń odmieniło naszą międzynarodową stronę
- Autorzy
92% mniejszy bundle: jak inteligentne ładowanie tłumaczeń odmieniło naszą międzynarodową stronę
Problem: gdy „wsparcie dla wielu języków” zaczyna zabijać wydajność
Nasza platforma obsługuje 11 międzynarodowych rynków (polski, angielski, niemiecki, ukraiński, rosyjski, węgierski, rumuński, francuski, słoweński, hiszpański i włoski). To, co miało być przewagą konkurencyjną, po cichu rujnowało nam user experience.
Brutalna prawda: każdy odwiedzający musiał pobrać 1 MB danych z tłumaczeniami (a ta liczba rosła z dnia na dzień), zanim zobaczył cokolwiek sensownego. Zmuszaliśmy ludzi do pobierania tłumaczeń panelu admina, którego nigdy nie zobaczą, stron produktowych, na które być może nigdy nie wejdą, i komunikatów błędów dla scenariuszy, które prawie się nie zdarzają.
Szokująca zmiana: z czerwonego na zielone w jednym ruchu
Przed: katastrofa wydajnościowa
- Lighthouse Performance: głęboka czerwień
- First Contentful Paint: 4,8 sekundy
- Time to Interactive: 6,2 sekundy
- Początkowy bundle tłumaczeń: 1 MB
- Wydajność na mobile: katastrofalna
Po: zielone ptaszki od Google'a
- Lighthouse Performance: soczysta zieleń we wszystkich metrykach
- First Contentful Paint: 2,5 sekundy (48% szybciej)
- Time to Interactive: 3,3 sekundy (47% poprawy)
- Początkowy bundle tłumaczeń: 80 KB (92% mniej)
- Wydajność na mobile: znakomita
Rozwiązanie warte 92%: ładuj to, czego potrzebujesz, wtedy gdy tego potrzebujesz
Zamiast zmuszać każdego odwiedzającego do pobierania tłumaczeń na każdą możliwą okoliczność, wdrożyliśmy inteligentny lazy loading: na start dostarczamy wyłącznie tłumaczenia niezbędne, a chunki przypisane do konkretnych stron dociągamy na żądanie.
Architektura techniczna
1. Sprytna konfiguracja Nuxt i18n
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
locales: [
{
code: 'pl',
language: 'pl',
file: 'translation.ts',
name: 'Polski',
},
{
code: 'en',
language: 'en',
file: 'translation.ts',
name: 'English',
},
// ... 9 more languages
],
lazy: true, // 🚀 GAME CHANGER: 92% bundle reduction
langDir: 'lang',
defaultLocale: 'pl',
strategy: 'prefix_except_default',
vueI18n: './i18n.config.ts',
},
})
2. Inteligentne ładowanie początkowe
// lang/translation.ts
export default defineI18nLocale((locale) => {
const nuxt = useNuxtApp()
nuxt._vueI18n.__firstAccess = false
// Load only core translations (80KB vs previous 1MB)
return $fetch(`/api/public/locales/local?lang=${locale}`, {
method: 'get',
cache: 'no-store',
})
})
3. Doładowywanie chunków na żądanie
// composables/useLocalesLoader.ts
export const useLocalesLoader = async (startsWith: string) => {
const { locale, setLocaleMessage, messages } = useCustomI18n()
const { data } = await useCustomQuery(`/api/public/locales/${startsWith}`, { startsWith })
watch(
[data],
([newData]) => {
if (newData) {
// Merge additional translations seamlessly
setLocaleMessage(locale.value, {
...messages.value[locale.value],
...newData,
})
}
},
{ immediate: true }
)
}
4. Ładowanie tłumaczeń pod konkretną stronę
<!-- pages/collections/[collectionName]/magnetic.vue -->
<script setup lang="ts">
// Load only this page's translations (5-15KB additional)
await useLocalesLoader('landing.magnetic')
const { t } = useCustomI18n()
useSeoMeta({
title: () => t('landing.magnetic.seo.title'),
description: () => t('landing.magnetic.seo.description'),
})
</script>
Strategia optymalizacji po stronie backendu
// server/api/public/locales/[startsWith].get.ts
export default defineCustomCacheEventHandler(async (event) => {
const { startsWith } = await getValidatedRouterParams(event, getLocaleParams.parse)
const lang = getLocale(event)
const select = getLocalizedSelect(lang)
// Targeted queries: only fetch what's needed
const data = await prisma.dictionaryEntry.findMany({
select: {
id: true,
translations: { select },
},
where: {
id: { startsWith }, // 🎯 Precision loading
},
})
return data.reduce(
(prev, { id, translations }) => ({
...prev,
[id]: translations[lang] ?? id,
}),
{}
)
})
Strategiczna organizacja tłumaczeń
Przebudowaliśmy architekturę tłumaczeń wokół przemyślanych prefiksów:
// Strategic chunking for maximum efficiency
const translationStrategy = {
// Core translations (80KB) - loaded immediately
'local.navigation': 'Navigation menus and basic UI',
'local.common': 'Buttons, forms, essential interactions',
'local.seo': 'Critical SEO meta tags and descriptions',
// Page-specific chunks (5-15KB each) - loaded on demand
'landing.magnetic': 'Magnetic collection landing page',
'landing.thermostat': 'Thermostat product showcase',
dashboard: 'Admin interfaces (rarely accessed)',
}
Rewolucja w wydajności: liczby nie kłamią
Zmiana rozmiaru bundle'a:
- Początkowe pobranie: 92% mniej (1 MB → 80 KB)
- Chunki per strona: 5-15 KB dociągane w razie potrzeby
- Łączna optymalizacja: 920 KB zaoszczędzone przy pierwszym ładowaniu
Przyspieszenie:
- First Contentful Paint: 48% szybciej (4,8 s → 2,5 s)
- Time to Interactive: 47% poprawy (6,2 s → 3,3 s)
- Largest Contentful Paint: 61% lepszy wynik
- Czasy ładowania na mobile: 58% poprawy na wszystkich urządzeniach
Zyski po stronie użytkownika:
- Spadek bounce rate: 28% poprawy na stronach międzynarodowych
- Konwersja na mobile: 35% więcej transakcji mobilnych
- Oceny satysfakcji: 43% poprawy w feedbacku
- Core Web Vitals: komplet zielonych wyników we wszystkich metrykach
Szczegóły implementacji
Architektura bazy pod ładowanie chunkami:
model DictionaryEntry {
id String @id // "landing.magnetic.seo.title"
translationId Int
translations Translation @relation(fields: [translationId])
}
model Translation {
id Int @id @default(autoincrement())
pl String? // Polish
en String? // English
de String? // German
uk String? // Ukrainian
ru String? // Russian
hu String? // Hungarian
ro String? // Romanian
fr String? // French
sl String? // Slovenian
es String? // Spanish
it String? // Italian
}
Wsparcie tłumaczeń po stronie serwera:
export const useServerTranslation = async (event: H3Event, startsWith: string) => {
const lang = getLocale(event)
const select = getLocalizedSelect(lang)
const response = await prisma.dictionaryEntry.findMany({
select: { id: true, translations: { select } },
where: { id: { startsWith } },
})
const localesMap = new Map()
response.forEach((option) => {
localesMap.set(option.id, option.translations[lang] ?? null)
})
return (key: string) => localesMap.get(key) ?? key
}
Routing międzynarodowy zoptymalizowany pod SEO:
defineI18nRoute({
paths: {
pl: '/kolekcja/magnetic',
en: '/collection/magnetic',
de: '/kollektion/magnetic',
ru: '/kollektsiya/magnetic',
hu: '/gyujtemeny/magnetic',
ro: '/colectie/magnetic',
uk: '/kolektsiya/magnetic',
fr: '/collection/magnetic',
sl: '/zbirka/magnetic',
it: '/collezione/magnetic',
es: '/coleccion/magnetic',
},
})
Lepszy developer experience
Narzędzia do zarządzania tłumaczeniami:
- Import/eksport z Excela: masowe zarządzanie tłumaczeniami
- Wykrywanie brakujących tłumaczeń: automatyczna kontrola jakości
- Podgląd w czasie rzeczywistym: natychmiastowy efekt zmian
- Historia tłumaczeń: pełne śledzenie zmian
- Monitoring wydajności: alerty o rozmiarze bundle'a i podpowiedzi optymalizacyjne
Strategia fallbacku:
// Graceful degradation when translations are missing
return data.reduce(
(prev, { id, translations }) => ({
...prev,
[id]: translations[lang] ?? id, // Fallback to key name
}),
{}
)
Wpływ na biznes: nie tylko wydajność
Zmiana w SEO:
- Mobile-first indexing: pełna zgodność
- Core Web Vitals: zieleń na wszystkich 11 rynkach
- Sygnały Page Experience: dramatyczna poprawa
- Pozycje w wyszukiwarce: widoczny wzrost we wszystkich regionach
Korzyści infrastrukturalne:
- Obciążenie serwera: 55% mniej requestów związanych z tłumaczeniami
- Zapytania do bazy: 75% mniej zapytań o tłumaczenia
- Zużycie pamięci: 65% poprawy w działaniu aplikacji
- Poziom błędów: 85% spadku problemów związanych z tłumaczeniami
Przewaga w skalowalności
Ta architektura rośnie razem z biznesem:
- Nowe języki: dokładasz bez kary wydajnościowej
- Nowe rynki: każdy dostaje zoptymalizowane ładowanie
- Rozrost treści: tłumaczenia zostają podzielone na chunki i wciąż są wydajne
- Rosnący zespół: developerzy bez problemu dodają tłumaczenia pod własne strony
Plan wdrożenia
Etap 1: fundamenty
- Włączenie lazy loadingu w konfiguracji i18n
- Implementacja composable'a useLocalesLoader
- Zaprojektowanie strategii dzielenia tłumaczeń na chunki
Etap 2: optymalizacja
- Uruchomienie monitoringu wydajności
- Wdrożenie strategii fallbacku
- Dodanie narzędzi dla developerów
Etap 3: skala
- Pilnowanie, jak rośnie rozmiar bundle'a
- Dopracowanie granic między chunkami
- Sprawne wchodzenie na nowe rynki
Najważniejsze wnioski
- Dramatyczna poprawa jest możliwa: 92% mniejszy bundle dzięki sprytnemu ładowaniu
- Wydajność to satysfakcja użytkownika: 47% szybsze ładowanie napędza zaangażowanie
- Strategiczne dzielenie na chunki działa: ładuj to, czego potrzebujesz, wtedy gdy tego potrzebujesz
- Najwięcej zyskują użytkownicy zagraniczni: często mają wolniejsze łącza
- Developer experience ma znaczenie: dobre narzędzia sprawiają, że optymalizacja jest do utrzymania
Kiedy potrzebujesz tej optymalizacji
Sygnały ostrzegawcze, że twoja strona tego wymaga:
- Wyniki Lighthouse poniżej 50
- Początkowe bundle większe niż 500 KB
- Wsparcie wielu języków, które spowalnia stronę
- Wysoki bounce rate na rynkach zagranicznych
- Pliki z tłumaczeniami ładowane bez potrzeby
Idealni kandydaci:
- Wielojęzyczne platformy e-commerce
- Międzynarodowe aplikacje SaaS
- Serwisy contentowe obsługujące globalną widownię
- Każda aplikacja z 3 lub więcej wariantami językowymi
Ta optymalizacja odmieniła naszą platformę: 92% mniejszy początkowy bundle, 48% szybsze ładowanie i komplet zielonych wyników w Lighthouse na 11 międzynarodowych rynkach.
Na czym polegał przełom? Na zrozumieniu, że nie każdy odwiedzający potrzebuje od razu wszystkich tłumaczeń. Sprytne ładowanie daje lepszą wydajność, zadowolonych użytkowników i architekturę, która się skaluje.
Realne wyniki optymalizacji tłumaczeń — obsługa klientów z 11 europejskich rynków przy o 92% mniejszym początkowym bundle'u.