Your store or blog is fast on desktop, but Lighthouse mobile reports an LCP (Largest Contentful Paint) over 10 seconds. You split the JavaScript bundle and compress images, and the score doesn't budge. Or you loaded Noto Serif KR through next/font/google to set one headline in a nice serif, and now thirty characters cost hundreds of KB. Both have the same cause: a Korean (or any CJK) web font.
Why does a Korean web font become the heaviest file on the page?
A Korean web font holds thousands of glyphs, so shipped as a single untouched file it outweighs all of the page's JavaScript. A Latin font has a few hundred glyphs and weighs tens of KB. Korean has 11,172 precomposed syllables, and even common fonts carry 2,350 or more. Each weight is another file. And web fonts sit on the path to painting text: if the font is late, headlines and body text are late, and LCP is late.
So for Korean, Chinese, or Japanese sites, check fonts before JavaScript in a mobile performance review. Filter the DevTools network tab by font and add up the transfer size.

Two ways to ship only the glyphs you use
Which one fits depends on whether you know in advance which characters will appear.
| Situation | Method | Result |
|---|---|---|
| Text keeps changing (blog, product copy, forums) | Font split into unicode-range slices | Only slices containing used characters download |
| Text is fixed (headline, logo text, a few labels) | Subset containing only the used glyphs | One file, tens of KB |
Sliced fonts (dynamic subsets). Split the font into many files by character range and give each @font-face a unicode-range. The browser downloads only the slices that contain characters actually on screen. Most CJK font distributors ship CSS in this form, so often the fix is one line: point to the sliced CSS instead of the single file.
Fixed-text subsets. If the on-screen text lives in your code, extract those characters and cut the font down to them. With fontTools pyftsubset it's one command:
# after collecting the characters used in your source into chars.txt
pyftsubset NotoSerifKR-Regular.ttf --text-file=chars.txt \
--flavor=woff2 --output-file=title-subset.woff2
In Next.js, load that file with next/font/local. As of 2026-10, next/font/google has no useful subsets value for Korean fonts, so it attaches the full sliced CSS and fetches a slice for every character on screen.
How do you notice glyphs missing from a subset?
A character missing from the subset silently renders in a system font. Nothing shows in the console; that one character just looks different. If whoever edits the copy forgets to re-bake the font, someone notices weeks later. So ship a test alongside the bake:
// every character in the UI source must be in the subset's character list
const used = collectCharsFromSource('app/**/*.tsx');
const baked = readFileSync('fonts/chars.txt', 'utf8');
const missing = [...used].filter((c) => !baked.includes(c));
expect(missing).toEqual([]); // on failure: re-bake the font subset
Checklist:
- Is the total
fonttransfer in the network tab no larger than the JavaScript total? - Does a fixed-text page attach the whole sliced CSS (check render-blocking CSS size too)?
- If you subset, is a "source characters ⊂ subset characters" test in CI?
- Are runtime-assembled strings (browser error messages, external API responses, user names) kept off that font? The test can't see them; use a sliced or system font there.
What happened to us
- A 12-second mobile LCP came from a single 2MB Korean font file — seven times all the JavaScript (300KB). Switching one line to sliced CSS took transfer from 2.4MB to 0.85MB and the mobile performance score from the 60s to the 80s.
- Thirty characters of headline and labels were pulling 13 slices, about 564KB.
next/font/googlehad attached 249 sliced@font-facerules (about 155KB of render-blocking CSS). Subsetting to the used glyphs and loading vianext/font/localbrought it to about 77KB, seven times smaller — with a missing-glyph test added alongside.