Google Fonts Slow in China? Self-Host and Subset CJK Fonts
Google Fonts is slow in China because the page first has to connect to fonts.googleapis.com for the CSS, then to fonts.gstatic.com for the font files. On many networks in China both domains connect slowly or fail outright, and that CSS blocks rendering by default. The fix is to download the font files and serve them alongside your site (self-hosting), then add subsetting and font-display. Below: the cause, the migration steps, how to handle Chinese fonts, and how to verify after launch.
Why do external fonts hold up the first paint?
A typical embed looks like this:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
The browser goes through these steps:
- It parses the
<link rel="stylesheet">. External stylesheets are render-blocking by default, so the page cannot paint until the CSS comes back. - It does DNS resolution, a TCP connection and a TLS handshake for
fonts.googleapis.com, then downloads the CSS. - The
@font-facerules in that CSS point tofonts.gstatic.com, a new domain, which means another round of DNS, TCP and TLS. - Only after the font file finishes downloading is the text rendered in the target font.
Steps 2 and 3 are two "first connections," and their cost has little to do with how big the requests are. On a good network each takes a few tens of milliseconds; on a bad one it can take seconds, or time out. Step 1 sets the worst case: if the CSS stalls, the whole page stays blank. display=swap only affects the font file (step 4); it does nothing about the render-blocking CSS.
One frequently cited old argument no longer holds: "a public font CDN lets you share cache with other sites." Major browsers now partition their caches by site (Chrome since version 86), so a font cached for site A is not reusable on site B. For visitors in China, an external Google Fonts link gives no caching benefit, only connection cost.
How do you measure how slow these two domains are for you?
Three ways to measure it yourself:
- Open the cdnping online speed test and look at the "first connect" column for public frontend CDNs. It includes DNS and the TLS handshake, which matches the cost of steps 2 and 3 above. "Round trip" is the median of 3 requests after the connection is reused. The bigger the gap between the two, the more an extra domain costs on your network.
- Open your own page, go to DevTools → Network, check Disable cache, filter by
fonts.g, and look at the DNS Lookup, Initial connection and SSL segments in each request's Timing tab. - On the command line, curl can break it down:
curl -o /dev/null -s -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n' \
https://fonts.gstatic.com/
This article gives no specific numbers: results depend on the ISP, region and time of day, so figures measured by someone else are of limited use to you. cdnping started collecting data on October 17, 2026, so the history is still short; see Methodology for how measurements are taken. If your whole site is slow in China, not just the fonts, start with how to diagnose a static site that is slow in China. For DNS-level anomalies, see how to tell DNS poisoning from DNS hijacking.
Steps to self-host Google Fonts
Most fonts on Google Fonts use the SIL Open Font License or the Apache License, which allow you to redistribute them. Before self-hosting, check the license of the specific font and keep its LICENSE file next to the font files.
Step 1: List the fonts and weights the page actually uses
grep -rn "fonts.googleapis\|fonts.gstatic" src/ public/ index.html
Write down each family and wght value. Many projects reference four weights (300, 400, 500, 700) but only use 400 and 700. Dropping the unused weights saves a few requests.
Step 2: Get the woff2 files
There are three ways, ordered from least effort to most manual.
Option A: the fontsource npm packages (for projects with a build step)
fontsource packages Google Fonts as npm packages, already split by weight and character subset, with the @font-face rules written for you:
npm i @fontsource/inter # static weights
# or the variable font version
npm i @fontsource-variable/inter
// main.js / main.ts
import '@fontsource/inter/400.css';
import '@fontsource/inter/700.css';
body { font-family: 'Inter', system-ui, sans-serif; }
After the build, the font files end up in dist/assets/ with hashed filenames. If npm install itself is slow, see how to set up an npm registry mirror first.
Option B: download automatically at build time
In a Vite project you can use a plugin such as vite-plugin-webfont-dl. At build time it downloads the Google Fonts links in your HTML and rewrites them to local paths, so the <link> in your source stays as it is. Note that the build machine must be able to reach those two Google domains; CI running overseas is usually fine.
Option C: download manually
The Google Fonts CSS endpoint returns different formats depending on the User-Agent. You need to send a modern browser's UA to get woff2:
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
curl -sA "$UA" 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap' -o inter.css
grep -o 'https://fonts.gstatic.com/[^)]*' inter.css | while read u; do
curl -sO "$u"
done
Then change the URLs in inter.css to your own paths, for example /fonts/xxx.woff2.
Step 3: Write @font-face and keep unicode-range
The CSS Google returns is already split by script (latin, latin-ext, cyrillic and so on), and each block carries a unicode-range. The browser only downloads the block that covers characters actually appearing on the page. When copying by hand, don't merge these blocks; only remove the ones you are sure you won't need (a Chinese-only site, for example, can drop cyrillic, greek and vietnamese):
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/inter-latin-400.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
woff2 alone is enough. All current mainstream browsers support it, so there is no need to also ship woff, ttf or eot.
Step 4: Preload the above-the-fold font, but don't overdo it
<link rel="preload" href="/fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
Two details: crossorigin is required even for same-origin fonts, otherwise the preloaded file is not reused and gets downloaded twice. And preload only the one file used by the body text above the fold. Preloading every weight competes with above-the-fold images and scripts for bandwidth and makes things slower.
Which font-display value should you use?
| Value | Behavior | Best for |
|---|---|---|
swap | Shows the fallback font immediately, swaps when the font arrives | Body text and brand fonts; the most common choice |
fallback | About 100 ms invisible period; swaps only if the font arrives within about 3 seconds, otherwise not for this visit | Body fonts, when you want less jumping if the font arrives late |
optional | If the font hasn't arrived within about 100 ms it isn't used, and is kept for the next visit (cached) | Fonts that are nice to have, with strict layout stability needs |
block | Up to about 3 seconds invisible, then falls back | Icon fonts; not recommended for body text |
The cost of swap is layout shift (CLS) when the font is swapped in. You can use size-adjust and ascent-override to make the fallback font's metrics close to the target font and reduce the jump:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
}
body { font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif; }
The percentages above are only examples; the real values have to be calculated per font. Some build tools have plugins that generate fallback metrics automatically, such as fontaine from unjs, which saves you the manual math.
What about Chinese fonts?
A Latin font weight is usually a few tens of KB. A Chinese font has to cover thousands to tens of thousands of characters, so a full file is often measured in MB, and referencing it without any processing will noticeably slow the first paint. Depending on your needs, there are three approaches, from lightest to heaviest.
Use system fonts whenever you can
For most body text, the Chinese fonts that ship with the operating system are good enough, with zero download:
body {
font-family: system-ui, -apple-system, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif;
}
Use a self-hosted Latin font for the English text and leave Chinese to system fonts. The only extra downloads are the few Latin font files.
Fixed text: subset to the characters actually used
For logos, headings, buttons and other fixed text, use fonttools to keep only the characters you need:
pip install fonttools brotli
# chars.txt holds every character the page actually uses
pyftsubset NotoSansSC-Bold.otf \
--text-file=chars.txt \
--flavor=woff2 \
--layout-features='*' \
--output-file=title-sc.woff2
A subset of a few dozen characters is usually only a few KB to a dozen or so KB. The downside is that you have to regenerate it whenever the copy changes, so it is best put into the build script: extract the characters from the templates and run it automatically.
Text that changes: slice by unicode-range
For blogs, documentation and other sites whose content changes all the time, the approach is the same one Google Fonts uses for Chinese, Japanese and Korean fonts: cut the whole font into a hundred or so small files, each with its own unicode-range, and the browser downloads only the slices containing characters that appear on the page.
- Use the fontsource Chinese packages directly, for example
@fontsource/noto-sans-sc. It follows Google's slicing scheme and works once installed. - For other Chinese fonts, you can slice them yourself with a dedicated tool (for example the open-source cn-font-split), which outputs woff2 slices and the matching CSS.
- When planning your own slices, a common approach is to take the 3,500 common characters from Level 1 of the Table of General Standard Chinese Characters (《通用规范汉字表》, China's official character standard) and put them in the first few slices in order of usage frequency, with rare characters in later slices, so an ordinary article triggers only a few slices.
More slices means more requests, so the site should preferably run HTTP/2 or HTTP/3 to let these requests share one connection.
Caching and response headers after self-hosting
Once the fonts are self-hosted, the cache headers decide whether returning visitors download them again. When build output filenames contain a hash, you can safely set a long cache lifetime:
location ~* \.woff2$ {
types { font/woff2 woff2; }
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
}
- Use
font/woff2as theContent-Type. - If filenames have no hash (for example the files you put in
/fonts/by hand in Option C), don't addimmutable; when you change a font, rename the file or add a version parameter. For what each directive means, see how to configure Cache-Control headers. - If the fonts live on a different domain from the page (say, a dedicated static asset domain), font requests are cross-origin and must return
Access-Control-Allow-Origin, or the browser will refuse to use them.
If your site itself is hosted overseas and is slow for visitors in China, the fonts will be no faster than the rest of it. In that case you need to look at how the whole site is delivered: to pick a public CDN, start with the CDN speed ranking and how to choose a public CDN for China. Acceleration that serves mainland China requires an ICP filing (China's mandatory website registration) and a licensed provider.
How do you verify after launch?
Go through this checklist item by item:
| Check | Method | Expected |
|---|---|---|
| No more requests to Google domains | grep -rn "fonts.googleapis|fonts.gstatic" dist/; filter fonts.g in DevTools | No results |
| Fonts are woff2 and same-origin | Filter by Font in Network and check Type and Domain | font/woff2, on your own domain |
| Preload is reused | No "preloaded but not used" warning in the Console; each file downloaded once | Only one request |
| Only needed slices are downloaded | Open a Chinese article and count Font requests | Only a few slices, not the whole set |
| Cache headers are correct | curl -I https://your-domain/assets/xxx.woff2 | max-age=31536000, plus immutable on hashed files |
| Layout shift is acceptable | Check CLS in Lighthouse or the Performance panel | No obvious jump when the font swaps in |
| First paint isn't blank on a weak network | Throttle to Slow 4G in DevTools and reload | Text appears in the fallback font first, without waiting for the font |
One easily missed spot: third-party components (comment widgets, chart libraries, documentation themes) may bring their own Google Fonts <link> or @import. After launch, filter for fonts.g again in production; that is more reliable than checking the source code alone.
Compare once after the change
After self-hosting, the page makes connections to two fewer external domains and drops a render-blocking third-party stylesheet. You can use the online speed test to compare the first-connect time of public CDN domains against your own domain, then compare first paint before and after the change in DevTools to confirm how much you actually saved. For other network-level checks, see the developer network self-check list (2026 edition).