A Latin font file is typically a few tens of kilobytes. A Korean font of the same quality is in a different league, and the reason is arithmetic rather than mystery.
Why Korean fonts are heavy
Modern Hangul composes 19 initial consonants × 21 vowels × 28 final positions (including “none”) = 11,172 syllable blocks. The Unicode Hangul Syllables block, U+AC00–U+D7A3, is exactly that range. Add the jamo themselves, Latin letters, digits and punctuation, and — for fonts that carry them — several thousand Han characters on top.
Where a Latin font holds a few hundred glyphs, a Korean font holds more than ten thousand. Yet any single page uses a small fraction of them; the syllables that appear in everyday Korean writing are heavily concentrated in a small part of the range.
What unicode-range does
unicode-range on an @font-face declares “this file covers this range of characters”. The browser looks at the characters actually present on the page and requests only the files responsible for them. The other declarations are parsed and never fetched.
The key is that many @font-face rules share one font-family name. In CSS it looks like a single typeface; on the network it is many small pieces.
/* Latin, digits, punctuation */
@font-face {
font-family: 'Pretendard';
src: url('/fonts/pretendard-latin.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0000-00FF, U+2000-206F;
}
/* One Hangul chunk — in practice dozens of these follow */
@font-face {
font-family: 'Pretendard';
src: url('/fonts/pretendard-korean-12.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
unicode-range: U+AC00-AF33;
}
⚠ One declaration without a range defeats the whole split. An @font-face that omits unicode-range claims every character, so that file is downloaded in full. When splitting appears not to work, look here first.
Static subset or dynamic split
There are broadly two approaches.
A static subset contains only the glyphs your site actually uses, in one file. It is the lightest option and it breaks when the content changes: publish a new article or add a product name containing a glyph you did not subset, and that character alone falls back to another typeface, mixing two faces inside one sentence. Fine for a landing page with fixed copy; risky for a site that keeps publishing.
A dynamic split cuts the Hangul range into many chunks and lets the browser take what it needs. There are more files, but each is small and any character is covered. For a site whose content grows, this is the right answer.
Preload only the first-screen chunks
With the split in place, the browser only requests a font after parsing CSS and meeting the text. For prominent first-screen text such as a hero heading, a preload is worth the head start.
<link rel="preload" as="font" type="font/woff2"
href="/fonts/pretendard-korean-12.woff2" crossorigin>
Two traps here. First, without the crossorigin attribute the preload is wasted. Fonts are fetched in CORS mode even from your own origin, so omitting it makes the browser download the same file twice — the console reports it as a preloaded resource that was never used.
Second, preloading every chunk undoes the split. A preload says “fetch this unconditionally”, so limit it to the handful of chunks your first-screen copy genuinely touches. Change the copy and the list goes stale — the day you rewrite a hero headline, check the preload list again.
The relationship between asset weight and first paint is covered further in the Performance archive, and you can see what a site actually downloads using the diagnostics in our free tools.
Next part
However finely you split it, a font still takes time to arrive. What happens to the text in the meantime — invisible, or shown in another face — is decided by font-display, and that is the next part.