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.
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.
Next.js SEO, 아무도 에러를 내지 않는 실패들
Next.js App Router에서 generateMetadata, OG 이미지, JSON-LD, sitemap이 경고 하나 없이 조용히 실패하는 패턴을 파헤친다. metadataBase 누락, 스트리밍 메타데이터가 body로 빠지는 함정, JSON-LD 스크립트 탈출, sitemap 5만 건 자동 절단까지 실무에서 반드시 알아야 할 모든 케이스를 정리한다.
Next.js 블로그를 운영한 지 3개월, 아직도 구글에 제대로 노출되지 않는다면
App Router 기반 블로그에서 generateMetadata 누락, OG 이미지 경로 오류, JSON-LD 렌더링 실패, sitemap 구성 실수 등 실제 배포 후에야 드러나는 SEO 취약 지점을 진단하고 수정하는 실전 체크리스트.
next/image sizes 한 줄이 LCP를 0.5초 당긴다
LCP 개선을 위해 무작정 이미지를 압축하고 CDN을 도입하기 전에, next/image의 sizes 속성과 priority 플래그가 실제로 어떤 영향을 미치는지 정량적으로 이해해야 한다. 이 글은 next/image 설정값이 LCP에 미치는 영향을 실제 코드 레벨에서 분석하고, 이미지 CDN이 진짜 필요한 상황과 불필요하게 최적화를 도입했다가 역효과를 보는 사례까지 함께 다룬다.
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.