Optimizing Web Font Loading
How to eliminate render-blocking type: WOFF2, screen-aware subsetting, critical CSS, and a real-world optimization walkthrough.
The Problem: Invisible Text
When a browser encounters an external font, it pauses text rendering until the file downloads. On a 3G connection, a 180 KB TTF for a Cyrillic display typeface can hold the page hostage for 4–6 seconds. Users see a blank rectangle — the Flash of Invisible Text (FOIT) — or worse, a jarring layout shift when the fallback suddenly swaps to the real typeface (FOUT). Both destroy perceived performance and accessibility.
The root causes are threefold. First, legacy formats like TTF and WOFF1 carry 30–50% more weight than modern WOFF2 for identical glyph sets. Second, most foundries ship full Unicode ranges — over 2,000 glyphs for Extended Latin + Cyrillic — even when a landing page needs only 48 characters. Third, the critical CSS that styles the above-the-fold hero is often inlined with a @font-face rule, meaning the browser cannot paint the headline until the typeface arrives.
Solutions That Ship
Every millisecond of text invisibility costs engagement. The following techniques, applied together, routinely cut first-text-paint from 2.4 s to under 400 ms on mid-tier mobile devices.
Serve WOFF2 Only
WOFF2 uses Brotli compression and font-specific optimizations. A 168 KB TTF for the typeface «Baskervville Cyrillic» drops to 42 KB in WOFF2. Drop TTF and WOFF1 from your @font-face stack unless you must support IE 11. Use a service worker or CDN rewrite to strip legacy formats for modern browsers.
font-display: swap
Add font-display: swap to every @font-face declaration. The browser renders immediately with the system fallback (e.g., system-ui, sans-serif), then swaps to the web font once it loads. On a 4G connection the swap happens in ~300 ms — imperceptible. Pair with font-display: optional for decorative headings where a one-time fallback render is acceptable.
Subset to Character Ranges
Extract only the glyphs used on the page. For a Russian-language checkout flow, the active set is U+0410–U+044F plus numerals and punctuation — roughly 128 glyphs. Tools like fonttools subset or Glyphhanger reduce a 42 KB WOFF2 to 9 KB. Screen-aware subsetting goes further: mobile viewports rarely display more than 600 characters above the fold, so ship a micro-subset for the hero and lazy-load the full set after interaction.
Preload Critical Fonts
Insert a <link rel="preload" as="font" href="/fonts/baskervville-cyrillic-bold.woff2" crossorigin> tag in the document head for the single weight used in the headline. This elevates the font to the highest download priority, beating CSS-parsed @font-face rules that the browser discovers mid-render. Preload only one or two files — over-preloading negates the benefit.
Inline Critical CSS, Externally Load Fonts
Inline the CSS that styles the above-the-fold content, but do not inline the @font-face block. Instead, defer the font stylesheet with <link rel="stylesheet" media="print" onload="this.media='all'">. The browser paints the hero with fallback type instantly, then upgrades to the web font. This pattern eliminates the render-blocking font download from the critical path entirely.
Conditional Loading by Viewport
Use CSS media queries to load font files only when needed. A decorative display weight at 72 px is useless on a 320 px screen. Define separate @font-face rules inside @media (min-width: 768px) blocks. Combine with IntersectionObserver to trigger font loading only when a section enters the viewport. This can save 60–80 % of total font bytes on mobile.
Tooling
Production-grade font optimization relies on a small, opinionated stack. These are the tools TypeLab uses internally and recommends for client projects.
Glyphhanger
A Node.js CLI that crawls your HTML, extracts used characters, and subsets fonts on the fly. Runs as a Gulp or Webpack plugin. Output: a production-ready WOFF2 with only the glyphs found in your markup. Actively maintained by the Google Web Fundamentals team.
fonttools (subset)
Python library for font manipulation. The subset subcommand reads a Unicode range or a text file and strips unused glyphs. Example: python -m fonttools.subset Baskervville-Bold.ttf --output-file=Baskervville-Bold.subset.woff2 --flavor=woff2 --layout-features+=kern,liga. Supports CID and variable fonts.
google-webfonts-helper
Web service that generates self-hostable @font-face kits from Google Fonts. Downloads WOFF2, WOFF, and TTF, writes the CSS, and bundles everything in a ZIP. Useful for TypeLab clients who want to self-host a licensed Cyrillic typeface without building a build pipeline.
Lighthouse / WebPageTest
Audit font loading with Lighthouse's "Eliminate render-blocking resources" and "Font display" audits. WebPageTest's filmstrip view shows exactly when text appears. Target: first text paint before 600 ms on Fast 3G, before 300 ms on 4G. Use these numbers as acceptance criteria before shipping.
Case Study: «Колорит» E-Commerce Redesign
A Moscow-based fashion retailer shipped a redesign with three custom Cyrillic typefaces from TypeLab: «Baskervville» for headlines, «Inter Cyrillic» for body, and «JetBrains Mono» for code snippets. The initial build loaded 540 KB of TTF fonts synchronously in the head. Time to first text paint on a Moto G4 (Fast 3G) was 3.8 seconds.
Step 1 — Format conversion. We converted all three typefaces to WOFF2 using fonttools. Total size dropped from 540 KB to 128 KB. We removed TTF and WOFF1 entirely; the client confirmed IE 11 traffic was 0.3 % and acceptable to degrade gracefully.
Step 2 — Subsetting. Glyphhanger crawled the 14 product pages, the checkout flow, and the blog. The active character set across all pages was 112 Cyrillic letters, 10 Latin initials, numerals, and common punctuation. Subsetting reduced the three WOFF2 files to 18 KB total.
Step 3 — Critical CSS + deferred @font-face. We inlined 4.2 KB of above-the-fold CSS (hero, navigation, product grid headers) and deferred the font stylesheet with the media="print" onload trick. The browser painted the hero headline in system-ui immediately, then swapped to Baskervville Bold within 210 ms on 4G.
Step 4 — Preload + font-display: swap. We added a single preload link for Baskervville Bold (the headline weight) and set font-display: swap on all three @font-face declarations. The preload elevated the headline font to the top of the download queue.
Result. First text paint on Fast 3G improved from 3.8 s to 490 ms — a 87 % reduction. Lighthouse Performance score rose from 41 to 94. Cumulative Layout Shift dropped from 0.18 to 0.02 because the fallback and web font had matched metrics (x-height, cap-height, and line-height were tuned in the CSS). The client reported a 6 % lift in checkout completion within two weeks of launch, attributed to the perceived speed gain on mobile.