Technote

Performance optimization Practical

Series Korean web typography Part 5 of 8

Subsetting Korean: not downloading all 11,172 syllables

There is no mystery to why Korean fonts are heavy: modern Hangul alone has 11,172 syllable blocks. Split with unicode-range, the browser fetches only the pieces a page actually uses.

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.

Conceptual — the exact ratio varies by document, but the direction never does

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.

For a site that keeps publishing, the decision is not a close one

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.

More on this topic

All technotes

Performance optimization Practical

Who pays for the milliseconds a tag adds?

A tracking script does not stop at downloading. While it is parsed and executed the main thread is busy, and a busy page ignores fingers. That cost is…

Marketers 5 min read

Performance optimization Practical

The object cache: what Redis actually removes

Repeated identical lookups disappear. One slow query does not. Miss that distinction and you will wire up Redis and then ask why nothing got faster.

Developers 7 min read

Performance optimization Practical

This is how a landing page gets heavy

Nobody decides to make a landing page heavy. It accumulates: one addition per campaign, and nothing ever removed when the campaign ends.

Marketers 6 min read

₩270,000 · Join the program