DevInsight

A developer's field notes

Frontend
0 viewsAbout 7 min read

Why font loading pushes the page sideways

Korean webfonts are several times heavier than Latin ones, which slows LCP, and when the font arrives late, the entire layout shifts and drags down the CLS score. This article covers subsetting to strip out unneeded glyphs, preload for resource priority, and font-display for the display strategy, with real-world criteria, and also looks at what Next.js next/font won't handle for you. The core point is to treat font optimization as part of the performance plan, not as a design concern.

Published by DevInsight.

#웹폰트#CLS#Core Web Vitals#성능 최적화#Next.js#font-display#preload#subset#layout shift

The moment a single Korean webfont is added to a page, the data the browser has to download typically jumps to about ten times that of a Latin font. There's no way a WOFF2 that packs all 11,172 precomposed Hangul syllables weighs the same as a Latin file that comes in at a few dozen KB. The real problem erupts when that heavy file arrives late. LCP gets pushed back, and the moment the font arrives, the whole screen lurches.

When a font declared in the stylesheet isn't available yet, the browser pre-renders the text with a fallback font. When the webfont arrives, it redraws that spot. The wider the difference in glyph width between the fallback and the webfont, the more violent that swap becomes. Korean precomposed and decomposed forms differ noticeably in glyph width, so the page visibly shifts compared to Latin. That's CLS (layout shift). The Core Web Vitals CLS threshold is 0.1. It's common for a single font swap to push the score past that number. When two or more weights are loaded, the shift happens twice, so the odds of exceeding the threshold go up accordingly.

A late font is worse than a heavy one

font-display is the switch that decides the timing of this swap. swap draws with the fallback font immediately and then swaps in the font when it arrives. Text shows up quickly, which helps LCP, but the swap moment produces CLS. optional applies this policy only to fonts that will arrive soon based on network conditions; in slow environments, the font is deferred to the next visit entirely. CLS disappears, but the design doesn't apply on the first visit. block and fallback create a period where text isn't visible at all, inflicting LCP damage that's hard to recover from.

Choosing swap puts CLS at risk, and choosing optional puts LCP at risk. Neither is smooth, so the practical move is to reframe the problem around eliminating the late-arrival situation itself. Shrinking the file and pulling the arrival time forward solve different points. Doing only the former leaves the font arriving late all the way through because priority gets demoted, and doing only the latter leaves a heavy file hogging bandwidth for a long time. The longer the delay, the more any font-display policy suffers. Even swap repeats the shift at each swap point, so any decision that assumes network latency should be based on the maximum-delay scenario.

The longer a font is delayed, the more the swap happens twice

On a first visit with an empty browser cache, fallback text stays on screen until the webfont arrives. Even with swap, if another element renders before the page's font load finishes, another shift occurs. If even a single font is delayed long enough, CLS accumulates across two separate shifts. You don't need two shifts to blow past the 0.1 threshold. One big shift is enough on its own.

font-display: optional skips this swap altogether. If the font doesn't arrive within the allotted time, the page is rendered entirely with the fallback for that visit. It's the only policy that separates the CLS problem from the font problem. But its downside is clear: the design font doesn't show on the first visit, which is hard to accept for a site that's sensitive to brand expression. Such sites use optional's alternative instead: shrink the font file itself so the arrival time falls inside the threshold. That way, even with swap, the shift stays small.

subset, cutting the file to half or less

Of the 11,172 characters in the file, the ones actually used are a tiny fraction. The top 2,000 or so characters cover most of what a Korean site consumes, and limiting scope to a single page brings that down to a few hundred characters. Subsetting is the process of rebuilding the font file with only those glyphs. pyftsubset from the fonttools family is the standard entry point.

pyftsubset NotoSansKR-Regular.otf \
  --text-file=page-text.txt \
  --flavor=woff2 \
  --layout-features='*'

This approach only works when you know the page text in advance. If unpredictable text is mixed in, like comments, usernames, or currency-rate tables, missing glyphs either pop out in the fallback font or render as tofu boxes. For services like that, reducing the number of weights is safer than carrying all the precomposed characters. When a weight is missing, the browser synthesizes it from a neighboring weight, and the widths end up off. That's CLS again. Two weights, roughly regular and semibold, are enough to serve; leave the rest to system fonts.

There's also the approach of splitting character ranges with unicode-range and downloading only the ranges you need. Korean glyphs are spread fairly evenly across the whole range, so the benefit is nowhere near what Latin gets. The more the files are split, the more downloads there are, and on legacy infrastructure without HTTP/2, parallel connection limits can backfire. Dynamic subsetting is another alternative, where the server takes the text to be rendered and sends only those glyphs; it fits services where the text changes significantly from page to page.

preload, and there's only one place to put it

preload raises a font's priority so the glyphs needed for the first screen arrive early. There's only one place to attach it: the font used by the LCP element. Preloading below-the-fold content or widgets that only appear after scrolling means the font eats all the spare bandwidth on a slow network and other resources fall behind. It's also only meaningful if the download starts ahead of stylesheet application. Putting <link rel="preload"> above the stylesheet tag is the recommended pattern. Conversely, without preload, the font only starts its journey when the CSS parser hits the font declaration.

If you preload more than one resource, the priority gets diluted. Stick to a single LCP font and let everything else follow the default load order. Keep the cache lifetime long too. Fonts rarely change versions, so a one-year Cache-Control header is fine. When the font comes from cache on the next visit, preload becomes unnecessary. But a cache policy alone doesn't fix the first-visit problem, so file size and priority tuning still matter.

What next/font can't strip away

Next.js's next/font self-hosts local fonts, attaches preload and font-display: swap automatically, and even estimates the fallback font's metrics to reduce the shift on swap. For a Latin font, this default behavior alone handles a large share of the work. The catch is that the subset next/font produces is mainly effective for Latin-family fonts with unicode-range defined. A Korean font is essentially carried into the bundle as-is. Just dropping a Korean webfont into next/font cuts the automation benefit in half, and the whole WOFF2 ends up in the initial load.

So in practice, the two steps are combined: build your own subset font with pyftsubset, keep it local, and load it through next/font. Referencing Google Fonts CDN is avoided, because even with preload, it often adds another HTTPS connection and lengthens TTFB. But this combination isn't a silver bullet either. If you have many pages whose content changes constantly, dynamic subsetting or self-hosting the full precomposed set is simpler. The downside of a subset font is that the file has to be rebuilt every time text is added, and missing-glyph detection has to be handled separately.

Verification starts with fallback font metrics

Apply the result and confirm it with numbers. First, match the fallback font's metrics to the webfont as closely as possible. When values like ascent, descent, and lineGap line up, the line-height difference on swap shrinks. That adjustment shows up in the CLS number immediately. One caveat: this imitation only approximates the overall width; it can't track the per-glyph width differences. Metric adjustment is a supporting measure; subsetting and preload, which pull the font's arrival time forward, are the main course.

Next, re-measure CLS with the web-vitals library or an inspection tool, and compare each font's transferred size and completion time in the network tab. It's common to see font files that are heavy but arrive fast, or light ones that arrive late. Until you look at the numbers, you can't tell which side is the bottleneck. That's why font optimization belongs in the performance plan, not at the design-deadline stage. Start by deciding which fonts the first screen uses, building the subsets, and fixing the preload target to a single line.

Comments

Loading comments.

Good Follow-up Reads

Posts connected to the topic you just read.

View all Frontend

Previous post

Why json_object is the Beginning, Not the End

DevInsight Digest

Keep every new article in one calm feed.

Follow the full publication feed without promotional alerts.

Subscribe to RSS