From bdf02b2a3a5e5ee754595d1cd317a6a922285b74 Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 6 Mar 2026 22:36:03 +0100 Subject: [PATCH] fix: eliminate 2s LCP rendering delay from Directus timeout on Hero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Hero server component awaited getMessages(locale) which called Directus with a 2-second timeout. On testing.dk0.dev (or when Directus is unreachable), this blocked the entire Hero render for ~2s → LCP 3.0s / 2320ms rendering delay. Changes: - Hero.tsx: remove getMessages() call entirely; use t() for all strings - messages/en.json + de.json: add hero.badge, hero.line1, hero.line2 keys - lib/i18n-loader.ts: invert lookup order — JSON first, Directus only as override for keys absent from JSON. Previously Directus was tried first for every key, causing ~49 parallel network requests per page load in HomePageServer (aboutT + projectsT + contactT + footerT translations). Now all JSON-backed keys return instantly without any network I/O. Co-Authored-By: Claude Sonnet 4.6 --- app/__tests__/components/Hero.test.tsx | 8 ++--- app/components/Hero.tsx | 16 +++------ lib/i18n-loader.ts | 47 ++++++++++++++------------ messages/de.json | 3 ++ messages/en.json | 3 ++ 5 files changed, 40 insertions(+), 37 deletions(-) diff --git a/app/__tests__/components/Hero.test.tsx b/app/__tests__/components/Hero.test.tsx index 756585a..3147380 100644 --- a/app/__tests__/components/Hero.test.tsx +++ b/app/__tests__/components/Hero.test.tsx @@ -5,6 +5,9 @@ import Hero from '@/app/components/Hero'; jest.mock('next-intl/server', () => ({ getTranslations: () => Promise.resolve((key: string) => { const messages: Record = { + badge: 'Student & Self-Hoster', + line1: 'Building', + line2: 'Stuff.', description: 'Dennis is a student and passionate self-hoster.', ctaWork: 'View My Work', ctaContact: 'Get in touch', @@ -13,11 +16,6 @@ jest.mock('next-intl/server', () => ({ }), })); -// Mock directus getMessages -jest.mock('@/lib/directus', () => ({ - getMessages: () => Promise.resolve({}), -})); - // Mock next/image interface ImageProps { src: string; diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 600b0e8..9337184 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -1,18 +1,12 @@ import { getTranslations } from "next-intl/server"; import Image from "next/image"; -import { getMessages } from "@/lib/directus"; interface HeroProps { locale: string; } -export default async function Hero({ locale }: HeroProps) { - const [t, cmsMessages] = await Promise.all([ - getTranslations("home.hero"), - getMessages(locale).catch(() => ({} as Record)), - ]); - - const getLabel = (key: string, fallback: string) => cmsMessages[key] || fallback; +export default async function Hero({ locale: _locale }: HeroProps) { + const t = await getTranslations("home.hero"); return (
@@ -29,15 +23,15 @@ export default async function Hero({ locale }: HeroProps) {
- {getLabel("hero.badge", "Student & Self-Hoster")} + {t("badge")}

- {getLabel("hero.line1", "Building")} + {t("line1")} - {getLabel("hero.line2", "Stuff.")} + {t("line2")}

diff --git a/lib/i18n-loader.ts b/lib/i18n-loader.ts index c8fe106..052cb85 100644 --- a/lib/i18n-loader.ts +++ b/lib/i18n-loader.ts @@ -29,8 +29,12 @@ function getCached(key: string): unknown | null { } /** - * Get a localized message by key - * Tries: Directus (requested locale) → Directus (EN) → JSON (requested locale) → JSON (EN) + * Get a localized message by key. + * Tries: JSON (requested locale) → JSON (EN) → Directus (requested locale) → Directus (EN) + * + * JSON is checked first so that translation-heavy server components never wait on + * a Directus network round-trip for keys that already exist in the message files. + * Directus is only queried when the key is absent from JSON (i.e. CMS-only content). */ export async function getLocalizedMessage( key: string, @@ -40,31 +44,16 @@ export async function getLocalizedMessage( const cached = getCached(cacheKey); if (cached !== null) return cached as string; - // Try Directus with requested locale - const dbValue = await getMessage(key, locale); - if (dbValue) { - setCached(cacheKey, dbValue); - return dbValue; - } - - // Fallback to EN in Directus if not EN already - if (locale !== 'en') { - const dbValueEn = await getMessage(key, 'en'); - if (dbValueEn) { - setCached(cacheKey, dbValueEn); - return dbValueEn; - } - } - - // Fallback to JSON file (normalize locale to 'en' or 'de') const normalizedLocale = locale.startsWith('de') ? 'de' : 'en'; + + // 1) JSON – requested locale const jsonValue = getNestedValue(jsonFallback[normalizedLocale as 'en' | 'de'], key); if (jsonValue) { setCached(cacheKey, jsonValue); return jsonValue; } - // Fallback to EN JSON + // 2) JSON – EN fallback if (normalizedLocale !== 'en') { const jsonValueEn = getNestedValue(jsonFallback['en'], key); if (jsonValueEn) { @@ -73,7 +62,23 @@ export async function getLocalizedMessage( } } - // Fallback: return the key itself + // 3) Directus – only for keys missing from JSON (CMS-only content) + const dbValue = await getMessage(key, locale); + if (dbValue) { + setCached(cacheKey, dbValue); + return dbValue; + } + + // 4) Directus – EN fallback + if (locale !== 'en') { + const dbValueEn = await getMessage(key, 'en'); + if (dbValueEn) { + setCached(cacheKey, dbValueEn); + return dbValueEn; + } + } + + // 5) Return the key itself as last resort return key; } diff --git a/messages/de.json b/messages/de.json index 954cef7..72e5f21 100644 --- a/messages/de.json +++ b/messages/de.json @@ -26,6 +26,9 @@ }, "home": { "hero": { + "badge": "Student & Self-Hoster", + "line1": "Building", + "line2": "Stuff.", "features": { "f1": "Next.js & Flutter", "f2": "Docker Swarm & CI/CD", diff --git a/messages/en.json b/messages/en.json index 3472ffe..d319dd4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -27,6 +27,9 @@ , "home": { "hero": { + "badge": "Student & Self-Hoster", + "line1": "Building", + "line2": "Stuff.", "features": { "f1": "Next.js & Flutter", "f2": "Docker Swarm & CI/CD",