perf(fonts): lazy-load CJK fonts per locale (~16MB font payload off the boot path)
Kanit (th), Noto Sans JP (ja) and Noto Sans KR (ko) were imported statically in main.tsx, so every visitor downloaded ~16MB of CJK font files. They're now loaded on demand once per locale (lib/cjkFonts.ts, wired in LanguageContext), so non-CJK users never fetch them and CJK users get their font after the first paint.
This commit is contained in:
parent
c95dd54a7a
commit
cc250b4d0b
4 changed files with 53 additions and 16 deletions
|
|
@ -2368,7 +2368,7 @@ similar are internal endpoints/hook names, not credits — leave them.)
|
||||||
is now **multi-way** — en/es stay key-for-key identical (unchanged), `th` (and
|
is now **multi-way** — en/es stay key-for-key identical (unchanged), `th` (and
|
||||||
`ja`) may be partial and are reported as **coverage %**; a `th`/`ja` key absent
|
`ja`) may be partial and are reported as **coverage %**; a `th`/`ja` key absent
|
||||||
from `en` fails.
|
from `en` fails.
|
||||||
- **Font:** `@fontsource/kanit` (Kanit) imported in `main.tsx` and added to the
|
- **Font:** `@fontsource/kanit` (Kanit), **lazy-loaded per locale** (`lib/cjkFonts.ts`) and added to the
|
||||||
CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow have **no Thai
|
CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow have **no Thai
|
||||||
glyphs**, so without it Thai silently falls back to a system font.
|
glyphs**, so without it Thai silently falls back to a system font.
|
||||||
- **Detection:** a Thai-device browser (`th*`) starts in `th` (`detectLanguage`).
|
- **Detection:** a Thai-device browser (`th*`) starts in `th` (`detectLanguage`).
|
||||||
|
|
@ -2388,7 +2388,9 @@ similar are internal endpoints/hook names, not credits — leave them.)
|
||||||
bloats the app. Each language is its own on-demand chunk.
|
bloats the app. Each language is its own on-demand chunk.
|
||||||
- **Fonts:** added **Noto Sans KR** (Korean) and **Inter** (Cyrillic/Russian) to
|
- **Fonts:** added **Noto Sans KR** (Korean) and **Inter** (Cyrillic/Russian) to
|
||||||
the CSS + Tailwind stacks; JP/KR/TH each need their own font (Outfit/Inter
|
the CSS + Tailwind stacks; JP/KR/TH each need their own font (Outfit/Inter
|
||||||
lack CJK).
|
lack CJK). **(2026-10-12) CJK fonts are lazy-loaded per locale** (`lib/cjkFonts.ts`,
|
||||||
|
wired in `contexts/LanguageContext.tsx`) — they're ~16 MB of font payload that
|
||||||
|
non-CJK users no longer download at boot.
|
||||||
- `check-i18n.mjs` now reports **all 12**; new locales are *partial* (coverage %)
|
- `check-i18n.mjs` now reports **all 12**; new locales are *partial* (coverage %)
|
||||||
until complete. Adding a locale = register in `language.ts` (`Language` union +
|
until complete. Adding a locale = register in `language.ts` (`Language` union +
|
||||||
`LANGUAGE_LOADERS`), `LanguageContext.detectLanguage`, `LanguageToggle`,
|
`LANGUAGE_LOADERS`), `LanguageContext.detectLanguage`, `LanguageToggle`,
|
||||||
|
|
@ -2408,7 +2410,7 @@ similar are internal endpoints/hook names, not credits — leave them.)
|
||||||
Both partial-locale rules below still hold for any *future* keys.
|
Both partial-locale rules below still hold for any *future* keys.
|
||||||
- **Partial by design** (same as `th`): a missing `ja` key falls back to English.
|
- **Partial by design** (same as `th`): a missing `ja` key falls back to English.
|
||||||
`check-i18n.mjs` reports `ja` coverage and fails if a `ja` key is absent from `en`.
|
`check-i18n.mjs` reports `ja` coverage and fails if a `ja` key is absent from `en`.
|
||||||
- **Font:** `@fontsource/noto-sans-jp` (Noto Sans JP) imported in `main.tsx` and
|
- **Font:** `@fontsource/noto-sans-jp` (Noto Sans JP), **lazy-loaded per locale** (`lib/cjkFonts.ts`) and
|
||||||
added to the CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow/Kanit have
|
added to the CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow/Kanit have
|
||||||
**no Japanese glyphs**, so without it Japanese falls back to a system font.
|
**no Japanese glyphs**, so without it Japanese falls back to a system font.
|
||||||
- **Detection:** a Japanese-device browser (`ja*`) starts in `ja` (`detectLanguage`).
|
- **Detection:** a Japanese-device browser (`ja*`) starts in `ja` (`detectLanguage`).
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
type Translations,
|
type Translations,
|
||||||
} from './language';
|
} from './language';
|
||||||
import { setDateLocale } from '@/lib/dateFnsI18n';
|
import { setDateLocale } from '@/lib/dateFnsI18n';
|
||||||
|
import { loadCjkFont } from '@/lib/cjkFonts';
|
||||||
|
|
||||||
interface LanguageProviderProps {
|
interface LanguageProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
@ -76,6 +77,12 @@ export function LanguageProvider({ children }: LanguageProviderProps) {
|
||||||
return () => { active = false; };
|
return () => { active = false; };
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
// Lazy-load the CJK display font for the active locale (ja/ko/th) — large font
|
||||||
|
// files that non-CJK users no longer have to download at boot.
|
||||||
|
useEffect(() => {
|
||||||
|
loadCjkFont(language);
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
const setLanguage = useCallback((lang: Language) => {
|
const setLanguage = useCallback((lang: Language) => {
|
||||||
setLanguageState(lang);
|
setLanguageState(lang);
|
||||||
setStoredLanguage(lang);
|
setStoredLanguage(lang);
|
||||||
|
|
|
||||||
38
src/lib/cjkFonts.ts
Normal file
38
src/lib/cjkFonts.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import type { Language } from '@/contexts/language';
|
||||||
|
|
||||||
|
const loaded = new Set<Language>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-load the CJK display font for the active locale.
|
||||||
|
*
|
||||||
|
* The Thai (Kanit), Japanese (Noto Sans JP) and Korean (Noto Sans KR) font
|
||||||
|
* families are large and only needed for users on those locales — but they were
|
||||||
|
* imported statically in main.tsx, so every visitor downloaded them. They are
|
||||||
|
* now loaded on demand, once per locale, and non-CJK users never fetch them.
|
||||||
|
* Text briefly falls back to a system font until the stylesheet resolves.
|
||||||
|
*/
|
||||||
|
export function loadCjkFont(language: Language): void {
|
||||||
|
if (loaded.has(language)) return;
|
||||||
|
loaded.add(language);
|
||||||
|
|
||||||
|
if (language === 'ja') {
|
||||||
|
void Promise.all([
|
||||||
|
import('@fontsource/noto-sans-jp/400.css'),
|
||||||
|
import('@fontsource/noto-sans-jp/500.css'),
|
||||||
|
import('@fontsource/noto-sans-jp/700.css'),
|
||||||
|
]).catch(() => {});
|
||||||
|
} else if (language === 'ko') {
|
||||||
|
void Promise.all([
|
||||||
|
import('@fontsource/noto-sans-kr/400.css'),
|
||||||
|
import('@fontsource/noto-sans-kr/500.css'),
|
||||||
|
import('@fontsource/noto-sans-kr/700.css'),
|
||||||
|
]).catch(() => {});
|
||||||
|
} else if (language === 'th') {
|
||||||
|
void Promise.all([
|
||||||
|
import('@fontsource/kanit/400.css'),
|
||||||
|
import('@fontsource/kanit/500.css'),
|
||||||
|
import('@fontsource/kanit/600.css'),
|
||||||
|
import('@fontsource/kanit/700.css'),
|
||||||
|
]).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main.tsx
16
src/main.tsx
|
|
@ -34,19 +34,9 @@ import '@fontsource/bebas-neue';
|
||||||
import '@fontsource/barlow-condensed/400.css';
|
import '@fontsource/barlow-condensed/400.css';
|
||||||
import '@fontsource/barlow-condensed/600.css';
|
import '@fontsource/barlow-condensed/600.css';
|
||||||
import '@fontsource/barlow-condensed/700.css';
|
import '@fontsource/barlow-condensed/700.css';
|
||||||
// Kanit — Thai glyphs (Latin/Thai stack fallback; the other fonts have no Thai)
|
// CJK fonts (Thai Kanit, Noto Sans JP, Noto Sans KR) are NOT imported here —
|
||||||
import '@fontsource/kanit/400.css';
|
// they're large and only needed for ja/ko/th users, so they're lazy-loaded per
|
||||||
import '@fontsource/kanit/500.css';
|
// locale (lib/cjkFonts.ts, wired in contexts/LanguageContext.tsx).
|
||||||
import '@fontsource/kanit/600.css';
|
|
||||||
import '@fontsource/kanit/700.css';
|
|
||||||
// Japanese glyphs — Noto Sans JP (Outfit/Inter/Bebas/Barlow/Kanit have none)
|
|
||||||
import '@fontsource/noto-sans-jp/400.css';
|
|
||||||
import '@fontsource/noto-sans-jp/500.css';
|
|
||||||
import '@fontsource/noto-sans-jp/700.css';
|
|
||||||
// Korean glyphs — Noto Sans KR
|
|
||||||
import '@fontsource/noto-sans-kr/400.css';
|
|
||||||
import '@fontsource/noto-sans-kr/500.css';
|
|
||||||
import '@fontsource/noto-sans-kr/700.css';
|
|
||||||
// Cyrillic (Russian) — Inter has Cyrillic; Outfit does not
|
// Cyrillic (Russian) — Inter has Cyrillic; Outfit does not
|
||||||
import '@fontsource-variable/inter';
|
import '@fontsource-variable/inter';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue