Optimise Google Fonts for production: self-host with fonttools subsetting, use the display=swap parameter, and measure the performance difference between CDN and self-hosting.
Google Fonts is the most widely used font service on the web — but the default embed code is a performance anti-pattern. The standard <link> embed triggers a cross-origin request, blocks rendering, and loads far more glyphs than you need. This lesson shows you how to get the fonts you want at the performance level you need, whether that means tweaking the CDN URL or going fully self-hosted.
The default Google Fonts embed snippet looks harmless but has several hidden costs. First, it makes a cross-origin request to fonts.googleapis.com to fetch a CSS file that then makes additional cross-origin requests to fonts.gstatic.com for the actual WOFF2 files. Cross-origin requests cannot reuse the browser's existing TCP connections to your origin server, adding 50-100ms of DNS lookup, TCP handshake, and TLS negotiation time per domain. Second, the CSS from Google's API does not include a preconnect hint by default — the browser discovers fonts.gstatic.com only after downloading and parsing the Google Fonts CSS file. Third, the API returns a large CSS file with unicode-range blocks for every language subset (Latin, Greek, Cyrillic, Vietnamese…) totalling 40-80KB even for a single font. None of this is catastrophic, but it adds up, especially on mobile connections.
1<!-- Standard embed — not optimised -->
2<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
3 rel="stylesheet">
4
5<!-- Better: add preconnect hints to start DNS/TCP early -->
6<link rel="preconnect" href="https://fonts.googleapis.com">
7<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
8<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
9 rel="stylesheet">The Google Fonts API accepts query parameters that control subset size and font-display. The display parameter sets font-display on the returned @font-face declarations: display=swap is the most important optimisation you can make to the default embed. The text parameter restricts the response to only the glyphs you specify — useful for logos or headings that use a limited character set. This can reduce a 30KB font file to under 5KB. The subset parameter restricts to specific unicode subsets (latin, latin-ext, cyrillic). These parameters compose:
1<!-- display=swap: eliminates FOIT, shows fallback immediately -->
2<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap"
3 rel="stylesheet">
4
5<!-- text= subsetting: only load glyphs used in a heading -->
6<!-- Result: 2-4KB instead of 30KB for this specific use case -->
7<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap&text=DeveloperBootstrap"
8 rel="stylesheet">
9
10<!-- Multiple fonts in one request to reduce RTTs -->
11<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Fira+Code:wght@400&display=swap"
12 rel="stylesheet">Self-hosting eliminates all cross-origin requests and gives you full control over caching headers. The trade-off is that Google's CDN has global edge nodes and many users already have Google Fonts cached — though privacy regulations have largely ended cross-site caching in modern browsers, making this benefit minimal. Two tools make self-hosting straightforward. google-webfonts-helper (gwfh.mranftl.com) lets you download font files and generated @font-face CSS in seconds. Fontsource (npmjs.com/org/fontsource) provides every Google Font as an npm package, importable directly in JavaScript.
1# Install a font via Fontsource
2npm install @fontsource-variable/inter1// Import in your app entry point (Next.js, Vite, CRA)
2import '@fontsource-variable/inter';
3// This automatically handles @font-face declarations
4// and places WOFF2 files in your public assets1/* If using google-webfonts-helper downloaded files: */
2@font-face {
3 font-family: 'Inter';
4 font-style: normal;
5 font-weight: 400;
6 font-display: swap;
7 src: url('/fonts/inter-v13-latin-regular.woff2') format('woff2');
8}For production applications, the most aggressive font optimisation is generating custom subset WOFF2 files that contain only the exact glyphs your content uses. fonttools is the Python library used by Google themselves to process fonts. pyftsubset is its command-line subsetting tool. A typical English UI application uses around 200-300 distinct glyphs. Stripping a full Latin font (800+ glyphs) down to your actual usage can reduce file size from 150KB to under 20KB.
1# Install fonttools
2pip install fonttools brotli
3
4# Subset to basic Latin + common punctuation + numerals
5pyftsubset inter-regular.ttf \
6 --output-file=inter-subset.woff2 \
7 --flavor=woff2 \
8 --layout-features='kern,liga,calt' \
9 --unicodes="U+0020-007E,U+00A0-00FF,U+2018-201E,U+2022,U+2026,U+20AC"
10
11# Subset to ONLY characters in your specific content file
12pyftsubset inter-bold.ttf \
13 --output-file=inter-bold-subset.woff2 \
14 --flavor=woff2 \
15 --text-file=content.txt # Reads chars from your actual content
16
17# Check resulting file size
18ls -lh inter-subset.woff2 # Compare to originalAfter optimisation, measure the actual difference using Chrome DevTools. In the Network panel, filter by 'Font' to see all font requests — note the file sizes and timing. Look at the Waterfall view to see when fonts start downloading relative to the HTML request. Use Lighthouse to see the 'Eliminate render-blocking resources' and 'Reduce the impact of third-party code' audits. Track the specific metric: Total Blocking Time (TBT) drops when cross-origin font requests are eliminated. CLS (Cumulative Layout Shift) drops when fallback stacks are well-crafted and font-display: swap/optional is used. LCP (Largest Contentful Paint) improves when fonts are preloaded and self-hosted.
1// Use the Font Loading API to measure font load time in JS
2document.fonts.ready.then(() => {
3 console.log('All fonts loaded');
4});
5
6// Detect if a specific font is loaded
7document.fonts.check('16px Inter').then(loaded => {
8 console.log('Inter ready:', loaded);
9});
10
11// Listen for individual font face loads
12const interFace = new FontFace(
13 'Inter',
14 'url(/fonts/inter-var.woff2)',
15 { weight: '100 900' }
16);
17document.fonts.add(interFace);
18interFace.load().then(() => console.log('Inter loaded'));Transform a default Google Fonts embed into an optimised self-hosted setup with correct display and preloading.
Next lesson introduces variable fonts — how a single WOFF2 file replaces your entire family of weight files and enables CSS animation along typographic axes.
Your progress is saved in this browser, on this device only.