
Getting web fonts on WordPress right matters more than most site owners think, because fonts are a triple hazard: they slow the first paint of every page, they cause the layout shifts that Google measures and penalises, and – if they load from Google’s servers to a visitor in the EU – they are a documented legal risk that has already produced court rulings and waves of warning letters in Germany. Yet the fix is neither expensive nor complicated. A site can keep exactly the typography its designer chose, load it faster than the Google CDN ever did, and remove the GDPR question entirely, usually in an afternoon.
I have done that afternoon’s work on a lot of client sites over 12+ years – business sites, WooCommerce stores, agency builds for clients in Germany, the UK, the US and Australia – and the pattern is always the same: too many font files, in old formats, loaded from third-party servers, with no loading strategy at all. This guide is the complete font hygiene routine I apply: how fonts actually load and why they block and shift, what the German Google Fonts rulings really said, how to self-host properly (woff2, @font-face, preload), which font-display value to choose, why subsetting and variable fonts are the biggest wins nobody uses, how many fonts a site honestly needs, and how to measure the difference afterwards.
Table of contents
- How web fonts load – and why they slow and shift pages
- The Google Fonts GDPR problem, in plain language
- Self-hosting fonts: the default answer
- font-display: which strategy to choose
- Subsetting and variable fonts: the biggest wins
- How many fonts and weights a site actually needs
- System font stacks: the completely free option
- Theme and builder font settings: where sites go wrong
- Icon fonts vs SVG icons
- Measuring what your fonts cost you
- The font hygiene checklist
- Frequently asked questions
How web fonts load – and why they slow and shift pages
A browser cannot download a font the moment it opens your page, because it does not yet know one is needed. It has to fetch the HTML, then the CSS, then parse the CSS to discover an @font-face rule, then check whether any visible text actually uses that family, and only then request the font file. That discovery chain is why fonts are consistently among the last critical resources to arrive, even though text is the first thing a visitor wants. If the font lives on a third-party domain such as fonts.gstatic.com, the browser also pays for a fresh DNS lookup, TCP connection and TLS handshake before a single byte of font data moves – easily 100-300 ms on a mobile connection, before the download itself.
While the font is in flight, the browser has to decide what to do with the text, and both options are bad:
- FOIT (flash of invisible text): the browser hides the text until the font arrives. Historically the default in Safari and Chrome for up to three seconds. The visitor stares at a page with headlines missing – which on a slow connection looks exactly like a broken site.
- FOUT (flash of unstyled text): the browser shows the text immediately in a fallback font, then swaps in the web font when it arrives. The text is readable from the start, but the swap can move things.
That swap is where layout shift comes from. Your web font and the fallback font almost never have identical metrics: the fallback might be slightly wider, taller or tighter. When the real font replaces it, a headline that wrapped onto two lines re-wraps onto one, everything below it jumps up, and the visitor’s thumb lands on the wrong button. Those jumps are measured as Cumulative Layout Shift (CLS), one of the three Core Web Vitals – I cover how Google scores it in my Core Web Vitals guide. Fonts are one of the two most common CLS causes I find in audits (the other is images without dimensions). And because headings are often the largest text on screen, a late font can also delay Largest Contentful Paint when the LCP element is a headline. Everything in this guide – self-hosting, preloading, font-display, subsetting – exists to shorten that chain and soften that swap.
The Google Fonts GDPR problem, in plain language
Google Fonts can be used two ways: you can download the font files and serve them yourself, or you can embed them the way the Google Fonts website suggests, with a stylesheet link that loads the files from Google’s servers on every visit. The second way is the legal problem, and it is worth understanding without the legalese.
When a visitor’s browser fetches a font from fonts.gstatic.com, it necessarily sends the visitor’s IP address to Google. Under the GDPR, an IP address counts as personal data. In January 2022 the Regional Court of Munich ruled that a website which loaded Google Fonts remotely had transferred a visitor’s personal data to Google without consent and without a valid legal basis, and awarded the claimant EUR 100 in damages. The amount was symbolic; the precedent was not. Within months, thousands of site owners in Germany and Austria received warning letters (Abmahnungen) demanding money for exactly this, some from opportunistic senders who automated the scanning. German data protection authorities have since repeatedly named remotely loaded Google Fonts as a compliance defect.
Three practical points follow from this:
- It applies to more than Google Fonts. Adobe Fonts (Typekit), font CDNs bundled with themes, and icon fonts loaded from third-party CDNs raise the same question: personal data flows to a third party before anyone consents to anything.
- A consent banner is a poor fix. Technically you can block fonts until consent, but then non-consenting visitors see fallback typography, your design breaks for a chunk of your audience, and you have added banner complexity to solve a problem that self-hosting removes outright. I go deeper on consent mechanics in the GDPR compliance guide.
- Self-hosting ends the discussion. Fonts served from your own domain send no visitor data anywhere. No third-party transfer, no consent requirement for fonts, no warning-letter surface. It is also faster. This is the rare case where the legal fix and the performance fix are the same fix.
I am a developer, not a lawyer, and nothing here is legal advice – but I build sites for German clients regularly, and self-hosted fonts have been the non-negotiable default on every one of them for years. No client has ever regretted it.
Self-hosting fonts: the default answer
Self-hosting sounds technical but is genuinely a small job. Here is the exact routine I use on client sites.
Step 1: find out what the site actually loads
Open the site in Chrome, open DevTools, go to the Network tab, filter by “Font” and reload. Note every file, its domain, its format and its size. Most sites are loading more than anyone intended – I regularly find 8-14 font files totalling 400-700 KB on sites whose design uses two families.
Step 2: download woff2 files only
For Google Fonts, the google-webfonts-helper tool (a well-known free web app) lets you pick a family, the weights you need and the character subset, and hands you a zip of files plus ready-made CSS. Take only the woff2 files. Every browser that matters has supported woff2 for years; ttf, otf, eot and plain woff are legacy baggage, and woff2 is typically 30% smaller than woff for the same glyphs. One format, no fallback formats, no exceptions.
Step 3: write the @font-face rules
Upload the files to your child theme (for example in a fonts folder) and declare each face:
@font-face {
font-family: 'Inter';
src: url('fonts/inter-v18-latin-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
One rule per weight and style, all pointing at your own domain, each with a font-display value (next section). If you build custom themes – or have one built, which is what my WordPress theme development service does – these rules belong in the theme’s base stylesheet from day one, not bolted on later.
Step 4: preload the one critical face
Preloading tells the browser about a font before it discovers it in the CSS, skipping the whole discovery chain. In the head:
<link rel="preload" href="/wp-content/themes/child/fonts/inter-v18-latin-regular.woff2" as="font" type="font/woff2" crossorigin>
Preload only the face used for above-the-fold text – usually the body regular or the heading weight, one file, occasionally two. Preloading everything is a classic mistake: preloads compete with the CSS and images for bandwidth, so five of them make the page slower, not faster. The crossorigin attribute is required even for same-origin fonts; leaving it off silently double-downloads the file.
Step 5: remove the old requests and verify
Now cut the Google requests: most decent themes have a “load Google Fonts” toggle to switch off; otherwise dequeue the style in the child theme, or use a plugin like OMGF that finds remote Google Fonts and replaces them with local copies automatically – a reasonable option for builder-heavy sites you do not want to open up. Then reload with the Network tab open and confirm two things: no request to any Google font domain, and no fonts loading twice. Finally check a page in a private window on a throttled connection – the text should appear instantly and settle without jumping.

font-display: which strategy to choose
The font-display property tells the browser what to do with text while its font is loading, and it is the single line of CSS with the most influence on how font loading feels. The honest summary of the options:
| Value | Behaviour while loading | Layout shift risk | Use when |
|---|---|---|---|
| swap | Fallback shown immediately, web font swaps in whenever it arrives | Moderate – one visible swap | Default for body text and most sites |
| optional | Tiny block period; if the font is not ready almost instantly, the fallback stays for this page view | Near zero | Strict CLS targets; font is nice-to-have |
| fallback | Very short block, short swap window, then fallback stays | Low | Middle ground when swap shifts too much |
| block | Text invisible up to 3 seconds waiting for the font | Low shift, but invisible text | Almost never – icon fonts only |
| auto | Browser decides, usually like block | Unpredictable | Never – always set a value explicitly |
My defaults: swap for branding-critical faces – visitors read immediately and the swap is acceptable if your fallback is chosen well – and optional for sites chasing a perfect CLS score, where returning visitors get the web font from cache and first-time visitors on slow connections get clean system type instead of a jump. You can soften swap’s shift further with CSS metric overrides: the ascent-override, descent-override and size-adjust properties let you tune the fallback font to occupy almost exactly the same space as the web font, so the swap barely moves anything. Tools exist that calculate these values per font pair; on sites where a headline swap was causing visible CLS, adding a tuned fallback took the font’s shift contribution to effectively zero.
Subsetting and variable fonts: the biggest wins
These two techniques deliver the largest byte savings in font work, and most WordPress sites use neither.
Subsetting: stop shipping alphabets you do not use
A font file ships glyphs for many writing systems: Latin, Latin Extended, Cyrillic, Greek, Vietnamese, sometimes hundreds of ligatures and alternates. A site publishing in English or German needs the latin subset and rarely anything else. The difference is dramatic: a full multi-script weight can be 250-300 KB where the latin-only woff2 of the same weight is 12-30 KB. Google’s own files are already subset per script – which is precisely why you pick “latin” in google-webfonts-helper instead of downloading everything. For non-Google fonts, command-line tools like pyftsubset (part of the fonttools package) or glyphhanger produce subsets, and the unicode-range descriptor in @font-face tells the browser which characters each file covers so it only downloads a subset when a character on the page needs it. One caution: keep the characters you do use – currency symbols, umlauts and other accented characters for German or French content, typographic quotes. A too-aggressive subset shows up as wrong-looking punctuation in a fallback font, which clients notice immediately.
Variable fonts: every weight in one file
A traditional family needs one file per weight and style – regular, medium, bold, plus italics, and suddenly you are at six or eight requests. A variable font packs a continuous range of weights (often 100-900, sometimes width and slant axes too) into a single file. One request, one cache entry, and every weight in between for free – including the in-between values like 450 or 550 that static families cannot offer. The single file is bigger than one static weight, but almost always smaller than the three or four statics it replaces: a typical latin-subset variable font runs 30-100 KB against 60-120 KB for the equivalent statics, with fewer requests. Most popular families – Inter, Roboto, Open Sans, Montserrat, Source Sans – have variable versions. If your design uses three or more weights of one family, the variable version is nearly always the right call; declare it once with a font-weight range in @font-face and let CSS pick any weight.
Put the whole stack together – self-hosted, woff2 only, latin subset, variable where it fits, one preload, font-display set – and the numbers move a long way. A recent client site went from 11 font requests and roughly 640 KB (two families, remote, full character sets, ttf fallbacks) to 3 requests and 84 KB, with no visible change to the design. That order of saving – 60-80% of font weight – is typical, not exceptional.

How many fonts and weights a site actually needs
Here is the honest answer nobody selling fonts will give you: two families and three to four weights in total cover almost every business site, store and blog I have ever built. A typical working set is one family for headings in one weight (say 700), and one family for body text in regular plus one emphasis weight – three files, or one variable file plus one static. Remember that every weight and every italic is its own file with its own download; “we might use light somewhere” costs every visitor on every page, forever.
- One family is completely respectable. A single well-chosen family in two or three weights looks deliberate and professional, loads fastest, and is the easiest to keep consistent. Many of the best-designed sites on the web use exactly this.
- Watch out for faux styles. If you use bold or italic text in a weight you have not loaded, the browser fakes it by slanting or smearing the real font – which looks slightly wrong everywhere. Load the italic if the site genuinely uses italics; otherwise do not write italics into the content.
- Three or more families is a red flag. It usually means the theme loads one, the builder another and the designer picked a third. Nobody chose that; it accumulated.
Pairing basics for non-designers
If you are choosing fonts without a designer, three rules produce safe, professional results. First, pair by contrast, not similarity: a serif for headings with a neutral sans-serif for body (or the reverse) reads as intentional; two similar sans-serifs read as a mistake. Second, let one font lead: the heading face carries the personality, the body face stays quiet and legible – never pick two fonts that both demand attention. Third, when in doubt, use one superfamily or one family at two very different weights: a 700 heading and 400 body of the same family always harmonises, because it cannot do anything else. And test your body face at 16-18px on a real phone paragraph before committing – display faces that look wonderful at 60px are often tiring at paragraph size.
System font stacks: the completely free option
There is a legitimate option that costs zero bytes, zero requests and zero legal thought: use the fonts already installed on the visitor’s device. A system font stack lists the native UI fonts of each platform and lets every device use its own:
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
Visitors on a Mac or iPhone see San Francisco, Windows shows Segoe UI, Android shows Roboto. These are excellent, professionally designed typefaces – and because they are already on the device, text renders instantly with zero layout shift and nothing for a GDPR audit to find. The trade-offs are real but narrow: the site looks slightly different per platform, and you give up typographic branding. My honest recommendation: system stacks are a strong choice for body text on content-heavy and speed-critical sites – documentation, blogs, intranets, dashboards – and an entirely reasonable choice everywhere for teams that value speed over type identity. A popular middle path I use often: one self-hosted brand font for headings, system stack for body. You get the recognisable brand voice at the top of the page and instant, shift-free paragraphs below it, with a single small woff2 as the entire font budget.
Theme and builder font settings: where sites go wrong
Most font bloat on WordPress is not chosen; it is a side effect of settings nobody reviewed. The recurring offenders from my audits:
- Themes that load every weight of their default family – a multipurpose theme enqueuing 100 through 900 plus italics “so any option works”, ten files of which the site uses three. Look for a theme option to select weights; if there is none, dequeue and self-host your own selection.
- Builders quietly re-adding Google Fonts. Elementor, Divi and others historically loaded any font picked in their controls from Google. Both now have switches – Elementor has a “load Google Fonts locally” setting and Divi a similar toggle – but on older installs these are off, and the site’s owner believes fonts were fixed years ago. Check the Network tab, not the settings page.
- Two sources for the same family. The theme loads Roboto locally, the builder loads Roboto from Google, and every page pays twice. This exact duplication shows up in perhaps a third of the builder sites I audit.
- Plugins with opinions. Sliders, review widgets, booking forms and table plugins that enqueue their own font or icon set sitewide for one shortcode on one page. Dequeue conditionally or replace the plugin.
- Customizer typography sections offering the whole Google catalogue – every experiment a client makes can add a family, and old choices linger enqueued after a redesign.
The fix routine is always the same: inventory what loads (Network tab, font filter), decide what the design actually requires, serve that minimal set locally, and switch off or dequeue everything else. On builder sites where dequeuing by hand is fragile, OMGF or the builder’s own local-fonts switch does the job acceptably. This pass is part of every audit in my WordPress speed optimisation service, and it is often worth 200-500 KB on the first page load.
Icon fonts vs SVG icons
Icon fonts deserve their own mention because they are fonts pretending to be images, and they inherit every problem in this guide. A full Font Awesome webfont bundle can weigh 300 KB plus across its files and styles – to display, on a typical site, a hamburger, a magnifier, three social icons and a phone. Icon fonts also fail differently: when the font is blocked or slow, visitors see empty squares or random letters where icons should be, and if it loads from a CDN you have another third-party transfer to explain. The modern answer is inline SVG: each icon is a few hundred bytes of markup pasted where it is used, styled with CSS (currentColor follows your text colour automatically), rendered instantly with no font request, no CLS and no GDPR surface. For the five to ten icons a normal site uses, inline SVG wins on every axis. If a theme or plugin insists on loading a full icon font sitewide, check whether it offers an SVG mode or a setting to disable the font, or dequeue it and replace the handful of icons manually – an hour of work that removes requests from every page view the site will ever serve.
Measuring what your fonts cost you
Before and after any font work, measure – otherwise you are guessing.
- The waterfall. Run the page through WebPageTest or read the DevTools Network panel. For each font: when was it discovered, how long did the download take, did it come from your domain, and did anything block on it? Late discovery (font requests starting after images) is the signature of the missing preload; a separate DNS and TLS block before the first font byte is the signature of third-party hosting.
- Total font weight and count. The Network tab’s font filter gives you both numbers. My working targets: under 100 KB and no more than 3-4 font files on a normal page. Over 200 KB, fonts are a real share of your page weight problem.
- CLS attribution. Chrome DevTools’ Performance panel records layout shifts and names the elements that moved; if the shifting elements are headlines and paragraphs, fonts are your CLS problem. PageSpeed Insights lists shift contributors in its diagnostics too, alongside the “ensure text remains visible during webfont load” audit that flags missing font-display.
- Real-world numbers. Lab tests use fast connections; check Core Web Vitals field data for CLS trends after your change, giving it a few weeks to settle.
To show the scale available: a German client’s brochure site I audited carried 640 KB of fonts across 11 requests, two of them from Google (a compliance concern for them on top of the speed cost). After the full routine – self-hosted, woff2, latin subsets, one variable font, one preload, swap with a tuned fallback – fonts cost 84 KB across 3 requests, mobile LCP improved by roughly 0.7 s, and CLS dropped from 0.19 to under 0.05. Fonts are rarely the only thing wrong with a slow site – the rest of the picture is in my complete WordPress speed guide – but they are frequently the cheapest big win on the list.
The font hygiene checklist
Run this top to bottom on any WordPress site; most take under half a day.
- Inventory: DevTools Network tab, font filter – list every file, domain, format, size.
- Confirm no font loads from Google or any third-party domain.
- Reduce to 2 families and 3-4 weights maximum; question every extra file.
- Serve woff2 only; delete ttf, otf, eot and woff variants.
- Subset to the scripts you publish in (latin, plus latin-ext if your language needs it).
- Use the variable version where you need 3+ weights of one family.
- Write clean @font-face rules in the child theme with explicit font-display.
- Preload exactly one critical face, with crossorigin set.
- Add fallback metric overrides if the swap still shifts headlines.
- Switch off theme and builder Google Fonts loading; dequeue duplicates.
- Replace icon fonts with inline SVG where practical.
- Re-test: waterfall, total weight under 100 KB, CLS attribution clean, no third-party font requests.
Want your fonts fast and worry-free?
Font hygiene is part of every speed project I deliver: I audit what your site loads, self-host and subset the fonts your design actually needs, fix the layout shifts and remove the Google Fonts compliance question – with before and after numbers you can check yourself. See the WordPress speed optimisation service and my portfolio, or send me your URL – I reply within 24 hours with what I would fix and a fixed price.
Frequently asked questions
Are web fonts bad for WordPress performance?
Not inherently – badly loaded fonts are. Self-hosted woff2 files, subset and preloaded with font-display set, cost very little; remote, unsubset fonts in many weights are one of the most common causes of slow rendering and layout shift.
Is it illegal to use Google Fonts on a WordPress site?
Loading them remotely from Google’s servers without consent was ruled a GDPR violation by a German court in 2022, and warning letters followed. Downloading the same fonts and self-hosting them is fully compliant, because no visitor data reaches Google.
How do I self-host Google Fonts on WordPress?
Download the woff2 files for the weights you need (google-webfonts-helper makes this easy), upload them to your child theme, declare them with @font-face, preload the critical face, and disable the theme or builder’s Google Fonts loading. A plugin like OMGF can automate it on builder sites.
How many fonts should a website use?
Two families and three to four weights in total is enough for almost any site. Every extra weight is another file every visitor downloads on every uncached visit.
What is the best font-display value?
swap is the right default: text is readable immediately and the font swaps in when ready. Use optional if you need near-zero layout shift, and never leave it unset, because browsers may then hide text while the font loads.
Do web fonts affect Core Web Vitals?
Yes, two of the three: late fonts delay LCP when a headline is the largest element, and the fallback-to-webfont swap causes CLS. Preloading, font-display and metric-tuned fallbacks address both.
Are system fonts good enough for a business site?
Often, yes. Platform fonts like San Francisco, Segoe UI and Roboto are professionally designed, load instantly and shift nothing. Many sites do well with a single brand font for headings and a system stack for body text.