perf: cache createLocaleConfigs per fallbackLocale value - #4130
perf: cache createLocaleConfigs per fallbackLocale value#4130Vincentdevreede wants to merge 1 commit into
Conversation
Walkthrough
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/runtime/shared/locales.ts`:
- Around line 16-19: Update createLocaleConfigs and localeConfigsCache to
enforce a bounded lifetime or size-based eviction policy, so request-specific
serialized fallbackLocale configurations cannot accumulate indefinitely during
SSR. Preserve cache reuse for retained entries while evicting older or expired
records according to the chosen limit.
- Around line 14-19: Bound the process-wide localeConfigsCache used by
createLocaleConfigs so distinct fallbackLocale keys cannot grow without limit.
Replace the unbounded Map behavior with a bounded FIFO or LRU policy, evicting
entries when the configured capacity is exceeded while preserving cached
results. Add a test covering insertion beyond capacity and verifying the oldest
or least-recently-used locale configuration is evicted.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 25f9ac3a-3742-4262-bbf1-eb55a263ffc0
📒 Files selected for processing (1)
src/runtime/shared/locales.ts
| const localeConfigsCache = new Map<string, Record<string, LocaleConfig>>() | ||
|
|
||
| export function createLocaleConfigs(fallbackLocale: FallbackLocale): Record<string, LocaleConfig> { | ||
| const cacheKey = JSON.stringify(fallbackLocale) | ||
| const cached = localeConfigsCache.get(cacheKey) | ||
| if (cached) { return cached } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -i 'locales|locale|i18n|runtime' . | head -200
printf '%s\n' '--- symbols and call sites ---'
rg -n --glob '!node_modules' --glob '!dist' \
'createLocaleConfigs|localeConfigsCache|LocaleConfig|FallbackLocale' .
printf '%s\n' '--- target file outline ---'
if command -v ast-grep >/dev/null 2>&1; then
ast-grep outline src/runtime/shared/locales.ts
fi
printf '%s\n' '--- target file ---'
cat -n src/runtime/shared/locales.tsRepository: nuxt-modules/i18n
Length of output: 15210
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- server context ---'
cat -n src/runtime/server/context.ts | sed -n '1,120p'
printf '%s\n' '--- client/server plugin ---'
cat -n src/runtime/plugins/i18n.ts | sed -n '1,90p'
printf '%s\n' '--- runtime context and mutation candidates ---'
cat -n src/runtime/context.ts | sed -n '1,55p'
cat -n src/runtime/context.ts | sed -n '145,185p'
cat -n src/runtime/context.ts | sed -n '300,325p'
printf '%s\n' '--- all writes and returned-object consumers ---'
rg -n --glob '!node_modules' \
'createLocaleConfigs\(|localeConfigsCache|localeConfigs\[|localeConfigs\.|serverLocaleConfigs|useLocaleConfigs\(\)\.value' \
src test specs
printf '%s\n' '--- relevant diff summary ---'
git diff --stat
git status --shortRepository: nuxt-modules/i18n
Length of output: 17323
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- setupVueI18nOptions and runtime config ---'
if command -v ast-grep >/dev/null 2>&1; then
ast-grep outline src/runtime/shared/vue-i18n.ts
fi
rg -n -A45 -B10 \
'setupVueI18nOptions|defineI18nConfig|fallbackLocale|runtimeConfig' \
src/runtime/shared/vue-i18n.ts src/runtime src/bridge docs/content/docs \
| head -500
printf '%s\n' '--- fallback type declarations ---'
rg -n --glob '*.{ts,d.ts}' \
'type FallbackLocale|FallbackLocale =' node_modules src . 2>/dev/null \
| head -120
printf '%s\n' '--- tests for shared locale helpers or runtime config requests ---'
rg -n --glob '*.{spec,test}.{ts,js}' \
'createLocaleConfigs|fallbackLocale|defineI18nConfig|runtime.*config|request' \
test specs | head -400Repository: nuxt-modules/i18n
Length of output: 44868
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- loadVueI18nOptions implementation ---'
if command -v ast-grep >/dev/null 2>&1; then
ast-grep outline src/runtime/shared/messages.ts
fi
rg -n -A80 -B15 \
'loadVueI18nOptions|vueI18nConfigs|ConfigLoader|i18nConfigs' \
src test specs | head -500
printf '%s\n' '--- request-scoped config examples ---'
rg -n -A25 -B12 \
'defineI18nConfig\(|useRequestEvent\(|useRuntimeConfig\(' \
specs test playground docs src | head -500
printf '%s\n' '--- cache-related tests and package scripts ---'
rg -n -A25 -B10 \
'cacheLifetime|cache|test' \
specs/lazy_load/message_isolation.spec.ts package.json vitest.config.* 2>/dev/null | head -300Repository: nuxt-modules/i18n
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -eu
node - <<'JS'
'use strict'
// Behavioral probe for the cache policy in src/runtime/shared/locales.ts.
// The locale configuration payload is intentionally small and representative;
// the point is whether distinct JSON keys remain strongly reachable.
const localeConfigsCache = new Map()
function createLocaleConfigs(fallbackLocale) {
const cacheKey = JSON.stringify(fallbackLocale)
const cached = localeConfigsCache.get(cacheKey)
if (cached) return cached
const localeConfigs = { en: { fallbacks: [fallbackLocale], cacheable: true } }
localeConfigsCache.set(cacheKey, localeConfigs)
return localeConfigs
}
const first = createLocaleConfigs({ default: ['en-0'] })
const sameValue = createLocaleConfigs({ default: ['en-0'] })
for (let i = 1; i <= 10000; i++) {
createLocaleConfigs({ default: [`en-${i}`] })
}
console.log(JSON.stringify({
equalValuesReuseEntry: first === sameValue,
entriesAfterDistinctInputs: localeConfigsCache.size,
firstEntryStillReachable: localeConfigsCache.get(JSON.stringify({ default: ['en-0'] })) === first,
lastEntryPresent: localeConfigsCache.has(JSON.stringify({ default: ['en-10000'] })),
}))
JS
printf '%s\n' '--- exact request/config call chain ---'
cat -n src/runtime/shared/messages.ts | sed -n '96,108p'
cat -n src/runtime/shared/vue-i18n.ts | sed -n '12,25p'
cat -n src/runtime/server/context.ts | sed -n '24,35p'Repository: nuxt-modules/i18n
Length of output: 2205
Bound localeConfigsCache before merge.
The server path resolves Vue I18n configuration for each request, and each distinct fallbackLocale key remains strongly reachable in the process-wide Map. Use a bounded FIFO/LRU cache and add an eviction test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/shared/locales.ts` around lines 14 - 19, Bound the process-wide
localeConfigsCache used by createLocaleConfigs so distinct fallbackLocale keys
cannot grow without limit. Replace the unbounded Map behavior with a bounded
FIFO or LRU policy, evicting entries when the configured capacity is exceeded
while preserving cached results. Add a test covering insertion beyond capacity
and verifying the oldest or least-recently-used locale configuration is evicted.
| export function createLocaleConfigs(fallbackLocale: FallbackLocale): Record<string, LocaleConfig> { | ||
| const cacheKey = JSON.stringify(fallbackLocale) | ||
| const cached = localeConfigsCache.get(cacheKey) | ||
| if (cached) { return cached } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 5 '\blocaleConfigs(?:\.value)?(?:\[[^]]+\])?\s*(?:=|\?\?=)|\.(?:fallbacks|cacheable)\s*=' src || trueRepository: nuxt-modules/i18n
Length of output: 4016
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- locale cache implementation ---'
cat -n src/runtime/shared/locales.ts | sed -n '1,120p'
printf '%s\n' '--- all localeConfigs references ---'
rg -n -C 4 '\blocaleConfigs\b|useLocaleConfigs' src
printf '%s\n' '--- type and interface declarations ---'
rg -n -C 5 'interface LocaleConfig|type LocaleConfig|FallbackLocale|useLocaleConfigs' srcRepository: nuxt-modules/i18n
Length of output: 23716
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Nuxt context consumers ---'
cat -n src/runtime/context.ts | sed -n '155,245p'
printf '%s\n' '--- server context and message loading ---'
cat -n src/runtime/server/context.ts | sed -n '45,85p'
rg -n -C 8 'function getMergedMessages|const getMergedMessages|getMergedMessages\s*=' src
printf '%s\n' '--- fallback property operations ---'
rg -n -C 4 '\bfallbacks\b|\bgetLocaleConfig\b' src --glob '*.{ts,js,vue}'
printf '%s\n' '--- static mutation verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
roots = [p for p in Path("src").rglob("*") if p.suffix in {".ts", ".js", ".vue"}]
patterns = [
r'\b(?:localeConfigs|serverLocaleConfigs|ctx\.localeConfigs|localeConfigs\.value)\s*(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)?\s*(?:=|\+=|-=|\*=|/=)',
r'\b(?:fallbacks|fallbackLocaleCodes)\s*\.\s*(?:push|pop|shift|unshift|splice|sort|reverse|copyWithin|fill)\s*\(',
r'\b(?:fallbacks|fallbackLocaleCodes)\s*\[[^\]]+\]\s*=',
]
for path in roots:
text = path.read_text(errors="replace").splitlines()
for number, line in enumerate(text, 1):
if any(re.search(pattern, line) for pattern in patterns):
print(f"{path}:{number}:{line}")
PYRepository: nuxt-modules/i18n
Length of output: 17351
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- repository-wide createLocaleConfigs references ---'
rg -n -C 4 '\bcreateLocaleConfigs\b|\blocaleConfigsCache\b' .
printf '%s\n' '--- complete merged-message implementation ---'
cat -n src/runtime/server/utils/messages.ts | sed -n '35,80p'
printf '%s\n' '--- repository-wide locale config writes and fallback mutators ---'
rg -n -C 3 '\b(?:localeConfigs|serverLocaleConfigs|ctx\.localeConfigs|fallbacks)\b' . \
--glob '!node_modules' --glob '!dist' --glob '!build' \
| rg '(^|:).*(:.*)?(=|\?\?=|\.push|\.pop|\.shift|\.unshift|\.splice|\.sort|\.reverse|\.fill|\.copyWithin)' || true
printf '%s\n' '--- tests mentioning locale config caching ---'
rg -n -C 5 'createLocaleConfigs|localeConfigsCache|fallbacks.*cache|cached-locale-configs' test tests . \
--glob '*.{ts,js,mjs,cjs}' 2>/dev/null || trueRepository: nuxt-modules/i18n
Length of output: 17668
Bound localeConfigsCache growth
localeConfigsCache retains one record for each distinct serialized fallbackLocale and never evicts entries. Different request-specific fallback configurations can therefore accumulate for the lifetime of the SSR process. Add a bounded eviction policy or another lifetime limit.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/shared/locales.ts` around lines 16 - 19, Update
createLocaleConfigs and localeConfigsCache to enforce a bounded lifetime or
size-based eviction policy, so request-specific serialized fallbackLocale
configurations cannot accumulate indefinitely during SSR. Preserve cache reuse
for retained entries while evicting older or expired records according to the
chosen limit.
|
We ran into the same "runs on every request" problem you describe here, and I wanted to share profiling data in case Context: we upgraded a Nuxt 3.15 monorepo to Nuxt 4.5.2 / @nuxtjs/i18n 10.6.0 and saw response times at our ingress go Profiling the production build under load (node --cpu-prof, requests for a single 442-byte JS chunk, 20 concurrent getEnv Sample distribution in that profile: ┌──────────────────┬─────────────────────────────────────┐ That's useRuntimeI18n(undefined, event) → useRuntimeConfig(event), which clones the whole runtime config and walks Measured impact, same build, only difference being a guard on the request hook: ┌───────────────────────────────────────────────────────────────┬───────┐ The guard we tested: const { buildAssetsDir } = useRuntimeConfig().app // resolved once at plugin setup nitro.hooks.hook('request', async (event) => { Static asset 404s still return 404 (Nitro throws those before the renderer, so render:before never runs and Worth noting our results don't contradict yours — they're complementary. We run 2–3 locales, and createLocaleConfigs Happy to open a separate issue with the full reproduction and profiles, and a PR for the guard, if that's useful. Not @BobbieGoede Could you take a look when you get a chance? I don't have much experience contributing, so I posted here since this looked like the same problem — happy to move it to a separate issue if that's a better place. |
Summary
createLocaleConfigsrecomputes every configured locale's fallback chain from scratch on every request that doesn't already have a cached i18n context,initializeI18nContextruns on Nitro'srequesthook unconditionally, so this happens on every single request. Its only real input,fallbackLocale, is normally the same value every time.This adds a small in-memory cache keyed by a serialization of
fallbackLocaleitself, rather than memoizing once and reusing forever.fallbackLocalecomes fromdefineI18nConfig, which can be async, so in theory it could differ per request, keying by value keeps that case correct (a different value just gets its own cache entry) while still skipping the work entirely whenever it's what it almost always is: unchanged.Benchmark
Measured directly against the real function (forcing a genuine cache miss on every "uncached" call versus repeated calls with the same value):
The cached path stays flat regardless of locale count, it's just a map lookup. The uncached path scales with locale count, since it walks every configured locale's fallback chain. Small in absolute terms per request, but free to keep, and it scales better the more locales are configured.
Summary by CodeRabbit