How to Optimize Web Fonts in Block Themes Using the theme.json Font Face API
Web fonts are the third-largest source of performance problems in block themes, after render-blocking scripts and images without explicit dimensions. A typical block theme that uses Google Fonts loads 200KB-400KB of font data, adds a cross-origin DNS lookup, and risks font-swap layout shifts on every page. The WordPress theme.json Font Face API, introduced in WordPress 6.0 and refined through 6.4, gives block theme developers a first-class way to host fonts locally, control display behavior, and eliminate the Google Fonts external dependency entirely. This article shows you exactly how to configure it, subset your fonts, preload critical weights, and measure the impact.
Why local font hosting beats Google Fonts for block themes
Google Fonts is convenient but carries three performance costs that add up quickly for block themes. First, the browser must resolve a DNS lookup for fonts.googleapis.com and fonts.gstatic.com before it can start downloading fonts. On a cold connection, that is 100-200ms of DNS + TCP + TLS time before a single byte of font data arrives. Second, the fonts.googleapis.com stylesheet is a render-blocking resource – the browser waits for it before continuing to parse the page. Third, cross-origin fonts cannot be cached as efficiently as same-origin assets in service workers.
Local font hosting eliminates all three problems. Fonts are served from the same origin as the page, share the same HTTP/2 connection, and can be cached by a service worker alongside other theme assets. For block themes built on the WordPress theme.json Font Face API, local hosting is the correct default. The Google Fonts external dependency should be the exception, not the rule.
Beyond raw latency, there are two additional reasons to prefer local hosting. First, the Google Fonts API has changed its font-display behavior multiple times. In 2020 it started honoring font-display=swap, but the exact character range subsets it delivers depend on the browser’s Accept-Language header, which means you can get different file sizes on different requests. Local hosting gives you complete control over which character ranges are bundled and which display strategy is applied. Second, the GDPR implications of loading Google Fonts, which sends the visitor’s IP address to Google’s servers, have become a legal consideration for sites with EU traffic. Local hosting eliminates the compliance question entirely.
The Lighthouse “Eliminate render-blocking resources” audit regularly flags Google Fonts stylesheets. Switching to local hosting via theme.json fontFace eliminates this finding entirely, because locally-loaded fonts do not require a separate stylesheet request.
Setting up font files in your block theme
Before configuring theme.json, you need the font files. Download the fonts you want to use from a source that permits free redistribution: Google Fonts (direct download from fonts.google.com, not the API), Font Squirrel, or the font foundry’s website. You need WOFF2 format for modern browsers. You do not need TTF, OTF, or EOT for block themes targeting WordPress 6.0+. Older formats exist for IE11 support, which block themes do not need to provide.
Create an assets/fonts/ directory in your theme root and place your WOFF2 files there. The naming convention matters for maintainability: use lowercase, hyphenated names that encode the font weight and style. For example: inter-regular.woff2, inter-bold.woff2, inter-italic.woff2, inter-semibold.woff2. This convention makes it obvious at a glance which weights and styles are present, and prevents the confusion that arises from using the original foundry filenames (which are often version-tagged UUIDs).
Once the files are in place, reference them in theme.json using the file:./assets/fonts/ path prefix, which resolves relative to the theme root. WordPress validates these paths at theme activation and generates the correct absolute URL at runtime, you never need to call get_theme_file_uri() or concatenate paths manually.
Configuring the theme.json Font Face API
The Font Face API lives under settings.typography.fontFamilies in theme.json. Each font family entry can declare one or more fontFace variants, each specifying a weight, style, display strategy, and source files. WordPress generates the corresponding @font-face declarations and adds them to the global styles inline block in the page <head>. Here is a complete configuration for Inter with three weights:
The fontDisplay field accepts the same values as the CSS font-display property: swap, optional, block, fallback, and auto. For above-the-fold content, use swap with a size-adjusted fallback (covered in article 2 of this series on eliminating layout shifts in block themes). For fonts that are not critical to the first viewport, use optional, which tells the browser to use the system fallback on the first load and silently upgrade on subsequent loads. This is the most aggressive strategy for perceived performance: the page renders immediately with a system font, the custom font loads invisibly in the background, and is used from the second visit onward.
The slug field in the font family declaration becomes the CSS custom property that references this font. If you declare a font family with "slug": "inter", WordPress generates --wp--preset--font-family--inter as a CSS variable. This variable is then available in all block style overrides in theme.json as well as in the Global Styles editor. Keeping slugs short, lowercase, and matching the font name makes the generated CSS readable.
Registering the font family for use in the editor
Once the font is declared in theme.json, it appears in the block editor’s Typography controls automatically. Site editors can assign the font to any block that supports custom typography via the Inspector Controls panel. The font also appears in the Global Styles typography panel, where you can set it as the default for body text, headings, or specific block types.
To set a font as the global default in theme.json, add a styles.typography.fontFamily reference to the slug you declared in settings. The pattern looks like "fontFamily": "var:preset|font-family|inter". You can also override the font per block type in the styles section: styles.blocks["core/heading"].typography.fontFamily for headings, or styles.elements.h1.typography.fontFamily for specific heading levels. This gives you complete typographic control from theme.json without writing any PHP or CSS. The same theme.json styles system that controls typography also controls spacing, color, and custom block supports for editor controls, font configuration is one part of a unified design token system.
What WordPress generates from your theme.json declaration
When WordPress processes the fontFace declarations in theme.json, it generates several outputs. First, it outputs @font-face rules in the Global Styles CSS block that is inlined in the page head. These rules point to the absolute URLs of your local font files. Second, it registers the font family as a CSS custom property (--wp--preset--font-family--{slug}) available throughout the site. Third, it makes the font available in the Font Library in the Site Editor (WordPress 6.5+), so administrators can see which fonts are active and remove unused ones. Understanding this output helps when debugging: if a font is not loading, start by checking the page source for the @font-face rule and verifying the URL is correct.
Font subsetting for reduced file sizes
The average WOFF2 font file for a full character set is 80KB-150KB per weight. An English-only site needs at most the Latin character range (U+0000-00FF plus a handful of additional punctuation characters). Subsetting removes all characters outside your target range, reducing the file to 15KB-30KB. That is a 70-80% reduction in font data for most Western European language sites. On a 3G mobile connection, the difference between a 120KB font and an 18KB font is about 400ms of load time.
The standard tool for subsetting is pyftsubset, part of the fonttools Python library. Install it with pip install fonttools brotli. Here is the command to subset Inter Regular to the Latin extended range, retaining all OpenType features:
The --layout-features="*" flag preserves all OpenType layout features (ligatures, kerning, small caps). The --no-hinting flag removes hinting data that is not needed for high-DPI screens, saving another 5-10% of file size. The --flavor=woff2 flag outputs WOFF2 directly using Brotli compression, which is why you need the brotli Python package alongside fonttools.
Run this as part of your theme’s build pipeline (Vite, webpack, or a simple npm script) so subsetting happens automatically when you update fonts. Commit pre-subsetted font files to your repository so the theme ships with the optimized versions and the build step is optional for theme users who do not modify the fonts.
For sites that support multiple languages, create a separate subset per language and load them using the unicode-range descriptor. This is exactly what Google Fonts does internally: it returns a different font file per browser based on the character ranges needed. You can replicate this with multiple fontFace entries in theme.json, each with a different unicodeRange value and a corresponding subsetted font file. The browser downloads only the subsets that contain characters actually present on the page.
Preloading critical fonts for faster rendering
Even after switching to local hosting and subsetting, fonts are still discovered late in the critical path. The browser cannot request them until it has parsed the CSS and found the @font-face declarations. The Global Styles block that WordPress outputs is an inline <style> tag in the page head, so font discovery is reasonably early, but it still comes after HTML parsing begins. For fonts used in above-the-fold content, this means a visible FOIT (flash of invisible text) or FOUT (flash of unstyled text) during the first few hundred milliseconds.
Font preloading solves this by telling the browser to start downloading the font at the highest priority as soon as the HTML begins parsing, without waiting for CSS. Add a <link rel="preload"> tag with wp_head priority 1 (before WordPress’s default priority of 10) to ensure it appears near the top of the document head:
The crossorigin="anonymous" attribute is required even for same-origin fonts when they are served from the same server. WOFF2 fonts are always fetched with CORS (even same-origin), and without this attribute the browser may fetch the font twice, once for the preload and once for the actual CSS-driven fetch, because the two requests have different credentials modes. Including crossorigin on the preload tag makes the CORS mode match the font fetch, allowing the browser to reuse the preloaded response.
Only preload the font weight that is used in visible text in the first viewport. Preloading too many fonts creates resource contention that can delay the LCP image or LCP text element. The rule of thumb: preload one font weight (usually regular/400), let all others load via the standard CSS discovery path. For variable fonts that cover multiple weights in a single file, preload that one file and skip individual weight preloads.
Preloading via theme.json (WordPress 6.4+)
WordPress 6.4 added a preview property to the fontFace declaration that tells WordPress which fonts to preload automatically. When set to true, WordPress generates the <link rel="preload"> tag without any PHP code required. This is the cleaner approach for block themes that want to keep all font configuration in theme.json rather than splitting it between theme.json and functions.php or mu-plugins. Add "preview": true to the fontFace entry for your regular/400 weight and verify the output in page source before removing the PHP filter.
Variable fonts in block themes
Variable fonts contain all weights and styles in a single file, with the variation axes embedded as data tables rather than as separate font files. A variable font for Inter covers every weight from 100 (Thin) to 900 (Black) in a single file of around 90KB, compared to 5-6 separate WOFF2 files totaling 120KB-180KB for the static equivalents. For block themes that use three or more font weights from the same family, a variable font is almost always the better choice: one HTTP request instead of six, one preload hint, and simpler theme.json configuration.
The theme.json configuration for a variable font uses a weight range ("100 900") instead of a specific weight value:
Variable fonts pair well with the fluid typography system in theme.json. When you combine a variable font with fluid font sizes (using clamp() values), you get a typography system that scales smoothly from mobile to desktop with a single WOFF2 file. The fluid: true flag at the settings.typography level enables clamp-based fluid sizing for all declared font sizes. This is one of the most impactful block theme performance improvements available: a complete responsive typography system with one HTTP request.
Not all fonts are available in variable format, but most major open-source families are: Inter, Source Sans Pro, Nunito, Raleway, Roboto Flex, Open Sans. Variable fonts work well alongside block theme template architecture, when you migrate widget areas to template parts, you often consolidate font loading by removing sidebar fonts that were previously loaded only on widget-bearing pages. Google Fonts provides variable font downloads for families that support them, look for the “Variable” badge on the font’s detail page. The variable version downloads as a single TTF or WOFF2 file with “VF” or “VariableFont” in the filename. Run it through pyftsubset with the same subsetting flags as static fonts to get the subsetted variable WOFF2.
Performance trade-offs with variable fonts
Variable fonts are not universally faster. For sites that use only one font weight (say, only regular/400), a single static WOFF2 file subsetted to Latin is typically smaller than the variable equivalent. Variable font files carry the axis tables and interpolation data for every intermediate weight, which has a minimum overhead of 15-20KB even for the simplest weight axis. The break-even point is roughly 2-3 weights: if you use two or fewer weights, compare file sizes between static and variable before committing to the variable format. If you use three or more weights, variable fonts win on total payload almost every time.
Measuring font performance impact
Before and after measurements matter. The key metrics to track for font performance are LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift), and the font-specific waterfall in WebPageTest. Run a Lighthouse audit before the optimization, note the LCP score, any render-blocking resource warnings, and the font-swap CLS contribution. Then run the same audit after switching to local fonts with preloading enabled.
In Chrome DevTools, open the Network panel and filter by “Font” to see font requests. Look for: (1) request start time, preloaded fonts should start within the first 500ms; (2) the file size, subsetted fonts should be under 30KB for Latin-only; (3) cache status on second load, fonts should come from the service worker or disk cache. A font that takes 800ms to start loading on a 4G connection is consuming a significant portion of the LCP budget.
WebPageTest’s waterfall view is more useful than Chrome DevTools for measuring font impact across connection speeds. Run a test at “Cable” (5Mbps, 28ms RTT) to establish a baseline, then run the same URL at “3G Fast” (1.6Mbps, 150ms RTT) to see how font loading degrades on slower connections. The delta between your preloaded local font and an unoptimized Google Fonts setup is largest on slower connections, where the cross-origin DNS lookup, TCP handshake, and TLS negotiation compound into 500-800ms of latency before the font file even starts transferring.
| Scenario | Font requests | Total font payload | Font start time (4G) |
|---|---|---|---|
| Google Fonts (unoptimized) | 3-4 (stylesheet + woff2) | 200-400KB | 400-800ms |
| Local hosting, no preload | 1-3 | 80-150KB | 150-300ms |
| Local, subsetted, no preload | 1-3 | 15-50KB | 150-300ms |
| Local, subsetted, preloaded | 1-3 | 15-50KB | 50-100ms |
| Variable font, subsetted, preloaded | 1 | 20-60KB | 50-100ms |
Cross-stack comparison: fonts across WordPress, Astro, and Shopify
Font optimization patterns are consistent across modern web frameworks, but the implementation details differ significantly. Understanding how other stacks approach the same problem helps block theme developers apply the right mental model and spot opportunities that the WordPress documentation does not highlight.
Astro’s built-in font optimization (introduced in Astro 5.0) automatically downloads Google Fonts, subsets them, and serves them locally with the optimal font-display strategy, the same workflow described in this article, automated at the framework level. You configure a font in the Astro config and the build step handles download, subsetting, and preload tag injection automatically. If you work across both Astro and block themes, you will recognize the same principles: local hosting, subsetting, preloading the critical weight. The difference is automation: Astro does it at build time, block themes require you to manage the font files and configuration manually.
Next.js’s next/font module handles Google Fonts the same way: it downloads the font at build time, subsets it to the character ranges your pages use, and serves it locally with zero external requests. The font is injected as a CSS custom property that you reference in your theme. In block themes, the theme.json Font Face API is the equivalent mechanism: it generates the CSS custom property and registers it in the editor. The key difference is that Next.js performs subsetting automatically based on what characters appear in your content, while block themes require you to determine the character range and run pyftsubset manually. This is an area where WordPress’s theme tooling lags behind the JavaScript framework ecosystem.
Shopify OS 2.0 themes use a similar pattern via the font_face filter and Liquid theme settings. Most Shopify themes default to system fonts, and many theme developers use variable fonts to reduce the total font payload when custom fonts are required. The Shopify CDN handles font serving with appropriate cache headers, which is equivalent to what you achieve with locally-hosted fonts served from a CDN-fronted WordPress host. The main difference is that Shopify handles font hosting infrastructure centrally; WordPress block theme developers are responsible for their own hosting setup and CDN configuration.
Common mistakes and how to avoid them
Several patterns appear repeatedly in block theme font implementations that cause measurable performance regressions. The most common is using font-display: block for body text. Block mode hides the text for up to 3 seconds while the font loads, causing a FOIT that Lighthouse penalizes heavily in the TBT (Total Blocking Time) metric. Reserve block for icon fonts where unstyled text would be completely unusable, and use swap or optional for body and heading fonts.
The second common mistake is preloading fonts that are not in the first viewport. If you preload a font weight used only in the footer or in a section below the fold, you are competing with LCP image loading for bandwidth. Every preloaded font adds to the critical resource queue that the browser must resolve before it can determine what the LCP element is. Preload only what appears above the fold on mobile, where the viewport is smallest and the LCP element most constrained.
Third: forgetting to remove the Google Fonts wp_enqueue_style call from functions.php after adding local fonts to theme.json. Both sources end up active simultaneously, resulting in double font downloads. The theme.json fonts load first (they are in the Global Styles block), then the Google Fonts stylesheet loads and overrides them. Check the Network panel after any font migration to confirm the Google Fonts requests are gone.
Fourth: using font-display: swap without a size-adjusted fallback. Swap shows the page text immediately in the system font, then swaps to the custom font when it loads. If the custom font has different metrics than the fallback, this causes layout shift, which is exactly the CLS problem you were trying to avoid by using swap. The fix is to configure a size-adjusted fallback that matches the metrics of your custom font, which is covered in detail in the CLS article in this series. Always pair font-display: swap with a size-adjusted fallback declaration in the same @font-face block.
Web font performance checklist for block themes
- Download font files (WOFF2) and place them in the theme’s
assets/fonts/directory - Remove any Google Fonts
wp_enqueue_stylecalls in functions.php - Configure fontFace in theme.json with
fontDisplay: "swap"for body fonts - Use
fontDisplay: "optional"for non-critical fonts (icons, decorative headings) - Subset fonts to Latin range using pyftsubset, reducing file size by 70-80%
- Add
rel="preload"for the single most critical font weight (regular/400) - Include
crossorigin="anonymous"on all font preload tags - Switch to a variable font if using 3+ weights from the same font family
- Enable fluid typography with the
fluid: trueflag in theme.json settings - Add a size-adjusted fallback alongside any
font-display: swapdeclaration - Verify in Lighthouse that “Eliminate render-blocking resources” no longer flags fonts
- Check WebPageTest waterfall to confirm font downloads start before 100ms
- Confirm fonts are served from same origin (no fonts.googleapis.com in Network panel)
What comes next in this series
With lazy loading (article 1), CLS elimination (article 2), and web font optimization (article 3) now covered, the next article addresses critical CSS: what it is in the context of block themes, how to extract above-the-fold styles from block output, and how to inline them to improve First Contentful Paint without increasing HTML payload. The final article provides a complete rendering audit workflow using Query Monitor and Chrome DevTools, tying together the measurement approaches touched on throughout this series.
The foundation for everything in this font article is the lazy loading and deferred assets setup covered in article 1. If you have not worked through the performance baseline from that article, start there before applying the font optimizations here, the improvements stack, and the measurement baseline from article 1 makes it possible to attribute the exact LCP improvement from each font optimization step.
Work with us on block theme performance
We help agencies and product teams optimize web fonts, improve Core Web Vitals, and build high-performance block themes across WordPress, Astro, and Shopify. If your block theme is still serving fonts via Google Fonts or has an LCP above 2.5s, get in touch. A focused performance engagement typically covers font optimization, script deferral, and image loading strategy in a single sprint, most teams see measurable LCP improvement within the first two days.