How to divide your font weight by 10 with subsetting

Most websites use custom fonts, often loaded from Google Fonts. As you probably know, you need to download as many files as you have weights. If you need 4 weights and their italic variants, that means 8 files (8 HTTP requests, 6 files of 150KB in TTF, etc.).

We'll see how to reduce the weight of your variable fonts by more than 90% (and improve your CLS (Cumulative Layout Shift)) while loading all weights in a single file.

We'll use Roboto as an example, and to compare performance, we'll convert the TTF files to WOFF2 to establish a baseline with optimized static fonts.

Static font TTF (source) WOFF2 (converted)
Roboto-Regular 156 KB 68 KB
Roboto-Bold 157 KB 70 KB
Roboto-Light 156 KB 68 KB
Roboto-Black 157 KB 69 KB
Roboto-Italic 162 KB 74 KB
Roboto-BoldItalic 163 KB 76 KB
Roboto-LightItalic 162 KB 74 KB
Roboto-BlackItalic 163 KB 75 KB
TOTAL 1.276 MB 0.574 MB

As you can see, font weight on a site is comparable to loading a dozen images.

Converting from TTF to WOFF2 brings a 55% gain — but it's not enough.

Let's see what we can achieve by switching to variable fonts instead.

What a variable font really is

A variable font is a single file that encodes multiple typographic variations along one or more continuous axes. The wght (weight) axis is the most common: it allows going from Thin (100) to Black (900) without downloading nine separate files.

Other axes exist: ital (italic), wdth (width), opsz (optical size), or proprietary axes. A font like Roboto Flex combines a dozen of them.

In practice, a variable TTF file contains:

One file for all weights (and much more) — but at what cost?

Variable font TTF (source) WOFF2 (converted)
Roboto-VariableFont_wdth,wght 477 KB 216 KB
roboto-italic-variablefont-wdth,wght 518 KB 249 KB
TOTAL 0.995 MB 0.465 MB

The variable version lets us slightly reduce font weight when using 4 or more weights. In other cases, it's the opposite — the variable font cost isn't worth it.

The real problem: unused glyphs

A Google Fonts file often covers hundreds of writing systems. Roboto, for example, includes basic Latin, extended Latin, Cyrillic, Vietnamese, Greek… all glyphs that an English or French website has absolutely no use for.

Subsetting means extracting only the glyphs corresponding to the characters actually used. For an English or French site, the following Unicode ranges cover 99.9% of needs:

Everything else? Removed. The gain is immediate and drastic.

Variable font TTF (source) WOFF2 WOFF2 Latin
Roboto-VariableFont_wdth,wght 477 KB 216 KB 77 KB
roboto-italic-variablefont-wdth,wght 518 KB 249 KB 89 KB
TOTAL 0.995 MB 0.465 MB 0.166 MB

Comparison with popular Google Fonts

Let's now compare the results across a sample of 5 popular variable Google Fonts.

Font Variable TTF (source) Full WOFF2 Latin WOFF2 Gain
Roboto 477 KB 217 KB 77 KB -84%
Open Sans 518 KB 274 KB 83 KB -84%
Inter 855 KB 341 KB 85 KB -90%
Montserrat 673 KB 202 KB 51 KB -92%
Roboto Flex 1646 KB 730 KB 288 KB -82%

Download optimized fonts

Here are a few fonts I've already slimmed down, ready to download and use on your own sites:

These ultra-optimized versions will speed up your site's loading time and improve your CLS (Cumulative Layout Shift).

Create your own font subsets

The reference tool is fontTools, the Python library maintained by Google and used internally to produce the Google Fonts themselves.

Installation:

sudo apt install python3-fonttools python3-brotli

or

pip3 install fonttools brotli

The subset command, as used in this project:

python3 -m fontTools.subset "resources/fonts/Lexend-VariableFont_wght.ttf" \
    --unicodes="U+0020-007F,U+00A0-00FF,U+0152-0153" \
    --flavor=woff2 \
    --output-file="public/fonts/lexend-variablefont-wght.woff2" \
    --layout-features="*" \
    --name-IDs="*"

What each option does:

Automated in a makefile, this gives a font-subset target called at build time. A single call to process all fonts in the resources/fonts/ folder:

font-subset: ## Generate a woff2 subset for each font in resources/fonts/ to public/fonts/
    @for font in resources/fonts/*; do \
        name=$$(basename "$$font" | sed 's/\.[^.]*$$//' | sed 's/_/-/g' | tr '[:upper:]' '[:lower:]'); \
        output="public/fonts/$$name.woff2"; \
        python3 -m fontTools.subset "$$font" \
            --unicodes="U+0020-007F,U+00A0-00FF" \
            --flavor=woff2 \
            --output-file="$$output" \
            --layout-features="*" \
            --name-IDs="*"; \
    done

Optimize your @font-face declaration

Now that the file is optimized, we can also optimize its loading in the @font-face declaration to improve perceived performance.

<!-- Preload: the browser downloads the font as soon as the <head> is parsed -->
<link rel="preload" href="/fonts/lexend-variablefont-wght.woff2" as="font" type="font/woff2" crossorigin>

<style>
    @font-face {
        font-family: 'Lexend';
        src: url('/fonts/lexend-variablefont-wght.woff2') format('woff2');
        font-weight: 100 900; /* all weights available on the wght axis */
        font-style: normal;
        font-display: optional; /* abandonne après > 100ms */
    }

    /* Calibrated fallback for zero layout shift */
    @font-face {
        font-family: 'Lexend Fallback';
        src: local('Arial'); /* display a calibrated Arial while waiting for Lexend */
        ascent-override: 90%;
        descent-override: 22%;
        line-gap-override: 0%;
        size-adjust: 107%;
    }
</style>

Time to clean up your fonts!

The recipe is simple:

  1. A variable TTF font in resources/fonts/
  2. A make font-subset at build time with fontTools → subsetted WOFF2 file in public/fonts/
  3. A properly declared @font-face with font-weight: 100 900 and font-display: optional
  4. A calibrated fallback with ascent-override, descent-override and size-adjust

The result on uxcode : Lexend at 28 KB, loaded in a single request, with all weights available from 100 to 900, no typographic flash, no layout shift.

The outcome? Rich typography, a single request under 30 KB, and one more best practice toward a Lighthouse score aiming for 100%.

This is the standard approach for any serious web project in 2026.