A problem you never encounter on English sites hits hard on CJK (Chinese, Japanese, Korean) sites. Loading Noto Serif KR via next/font/google added 26KB of render-blocking CSS. The same approach with a Latin font? Just 1–2KB.
This article explains why CJK web fonts have such a large performance impact, and documents the process of reducing 124 @font-face rules to 1, cutting render-blocking CSS from 26KB to 3KB.
124→1
@font-face rules
26→3KB
Render-blocking CSS
0.095→0
CLS eliminated
+8pts
Desktop score gain
1The Problem — 124 @font-face Rules
The standard way to use Google Fonts in Next.js is next/font/google. The code is straightforward:
import { Noto_Serif_KR } from "next/font/google";
const notoSerif = Noto_Serif_KR({
subsets: ["latin"],
weight: ["400", "700"],
variable: "--font-noto-serif",
});Open the CSS this generates at build time and you will be surprised. It contains 124 @font-face rules. Each specifies a different unicode-range and references a different woff2 file.
// Generated CSS (a few of 124)
@font-face {
font-family: 'Noto Serif KR';
src: url(/_next/static/media/xxx1.woff2) format('woff2');
unicode-range: U+f9ca-fa0b, U+fa0e, ...;
}
@font-face {
font-family: 'Noto Serif KR';
src: url(/_next/static/media/xxx2.woff2) format('woff2');
unicode-range: U+d723-d728, U+d72a-d72b, ...;
}
/* ... 122 more ... */This entire CSS block is a render-blocking resource. The browser will not paint anything until it has downloaded and parsed all of it. Adding 26KB of extra CSS parsing on slow 4G adds hundreds of milliseconds. For a deeper look at how render-blocking affects metrics, see Core Web Vitals debugging.
2Why CJK Fonts Are Different
The Latin alphabet has roughly 100 characters. Korean Hangul alone has 11,172 possible syllables. Chinese and Japanese character sets are similarly massive. This single difference explains everything.
| Metric | Latin Font | CJK Font |
|---|---|---|
| Glyph count | ~100 | ~11,172+ |
| Full font size (woff2) | ~20KB | ~2MB |
| Subset splits | 1–2 | 60–124 |
| @font-face rules (2 weights) | 2–4 | 120–250 |
| Render-blocking CSS | ~1KB | ~26KB |
Google Fonts splits CJK fonts into many unicode-range subsets so the browser only downloads the chunks it needs. The idea is sound, but the @font-face rules themselves are included in render-blocking CSS. Even subsets that are never downloaded still get parsed.
3Solution: Custom Subsetting
The core idea: build a font file containing only the characters your site actually uses. Not all 11,172 — just the few dozen to few hundred you need.
Step 1: Extract Used Characters
Scan the text rendered with the target font and collect the unique CJK characters. For a site using a serif font only in headings and pull quotes, this is often just a few hundred characters.
Step 2: Generate Subset with Google Fonts text API
Google Fonts has a little-known feature: pass a text parameter and it returns a font containing only those characters.
// Google Fonts text API
// This URL returns CSS with only the specified characters
https://fonts.googleapis.com/css2
?family=Noto+Serif+KR:wght@700
&display=swap
&text=가나다라마바사아자차카타파하...
// The returned CSS contains exactly 1 @font-face rule
// → Extract the woff2 URL and download itStep 3: Load with next/font/local
import localFont from "next/font/local";
const notoSerif = localFont({
src: "../public/fonts/NotoSerifKR-Bold.woff2",
weight: "700",
style: "normal",
variable: "--font-noto-serif",
display: "optional", // critical!
fallback: ["Georgia", "Times New Roman", "serif"],
});Before: next/font/google
- 124 @font-face rules
- ~26KB render-blocking CSS
- Both weight 400 + 700 included
After: next/font/local
- 1 @font-face rule
- ~3KB render-blocking CSS
- Only the weight actually used
Remove unused weights
The original code loaded weight 400 and 700. But a grep through the codebase revealed the serif font class was always paired with font-bold — weight 400 was never used. Dropping it cut the @font-face count in half before any other optimization.
4font-display Strategy
What happens before the font loads? This choice directly affects both CLS and LCP.
| Value | Behavior | CLS | LCP |
|---|---|---|---|
| swap | Show fallback → swap when font loads | Risky | Re-measured |
| optional | Use font if loaded within 100ms, else keep fallback | Safe | Safe |
| block | Hide text until font loads (up to 3s) | Safe | Delayed |
The problem with swap
swap is the most commonly used setting, but it has two issues. First, if the fallback font (e.g., Georgia) and the web font (Noto Serif KR) have different glyph sizes, text reflows at the swap moment — that is CLS. Second, the size change causes the browser to re-measure LCP. You can end up with FCP at 1.5s but LCP ballooning to 3.2s after the font swap.
Why optional is the answer
optional gives the font a 100ms window. If it loads in time, it is used. If not, the fallback stays. No font swap occurs, so CLS is zero and LCP is never re-measured.
“But will the web font ever appear?” — When self-hosting a custom subset, the font loads from the same server, so it arrives within 100ms on most connections. On extremely slow networks where it would not load in time, the fallback is actually a better UX anyway.
5Experiment: Google CDN Direct Load
“Google's global CDN is fast — would loading directly from there be faster than self-hosting?” A reasonable hypothesis. We tested it.
// Added to layout.tsx <head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Serif+KR:wght@700&display=optional"
rel="stylesheet"
/>Result: Mobile 80 → 69 (11-point drop)
Even with preconnect, DNS lookup + TLS handshake to an external domain costs 200–400ms. Under Lighthouse's slow 4G throttling, this overhead is devastating. Google CDN excels at global caching, but PageSpeed simulations run without cache.
Conclusion: Always self-host
Whether you use next/font/google or next/font/local, Next.js font system serves font files from the same domain. Never use an external CDN. Same-origin loading is always faster in PageSpeed.
6Results
| Metric | Before | After |
|---|---|---|
| Mobile Performance | 70 | 74 |
| Desktop Performance | 88 | 96 |
| CLS (Mobile) | 0.095 | 0 |
| @font-face rules | 124 | 1 |
| Render-blocking CSS | ~26KB | ~3KB |
| Font weights | 400 + 700 | 700 only |
Mobile gained 4 points and Desktop gained 8 — modest numbers on their own. But the critical win was CLS dropping to exactly 0. A previous fix had reduced CLS from 0.5 to 0.095, but that residual 0.095 was caused by font swapping. Switching to font-display: optional eliminated it completely.
Desktop jumping from 88 to 96 was the direct result of reduced render-blocking CSS. Desktop simulations assume fast networks, so the CSS parsing time reduction translated directly into FCP improvement.
Key Takeaways
- ✓CJK fonts generate 60–120x more @font-face rules than Latin fonts
- ✓Google Fonts text API can create custom subsets containing only your actual characters
- ✓next/font/local + custom subset: 124 @font-face → 1, CSS 26KB → 3KB
- ✓font-display: optional prevents font swap → CLS 0 + no LCP re-measurement
- ✓Google CDN direct load actually dropped the score by 11 points — always self-host
Data was measured in February 2026. Font subset sizes vary depending on the actual characters used. The Google Fonts API text parameter behavior may change. Non-commercial sharing of this content is welcome. For commercial use, please reach out via our contact page.
Need help optimizing your site performance?
Treeru handles everything from CJK font optimization to full Core Web Vitals improvement.
Request a Free Consultation