ehsan.blog
~/blog/how-to-avoid-render-blocking-fonts-with-a-self-hosted-variable-font — zsh
cat how-to-avoid-render-blocking-fonts-with-a-self-hosted-variable-font.md

How to avoid render-blocking fonts by self-hosting a variable font

·4 min read

The classic way to add a custom font is to paste a <link> to Google Fonts in your <head>. It works, but it quietly slows your page down and hands a third party some control over your rendering. On my portfolio I self-host JetBrains Mono instead — one import line, bundled by Vite, and no external request. Here’s why that’s faster and how to do it.

The usual snippet looks like this:

html
<!-- The render-blocking pattern -->
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap" rel="stylesheet" />

A <link rel="stylesheet"> in the <head> is render-blocking — the browser will not paint the page until it has fetched and processed that stylesheet. And here it’s worse than a normal local stylesheet, because of the chain of requests hiding behind that one line:

sequenceDiagram
  participant B as Browser
  participant G as fonts.googleapis.com
  participant S as fonts.gstatic.com
  B->>G: 1. DNS + connect + fetch CSS
  G-->>B: CSS with @font-face URLs
  B->>S: 2. DNS + connect + fetch .woff2
  S-->>B: font file
  Note over B: only now can text render in the real font

That’s two separate third-party domains, each needing a DNS lookup, a TCP connection, and a TLS handshake before a single byte of font arrives. On a slow connection those round-trips add up, and until then your text either doesn’t show or shows in a fallback and then jumps (layout shift).

The fix: install the font and import it

Fontsource packages open-source fonts as npm modules. I install the variable version of JetBrains Mono and import it once in my app entry point:

tsx
// src/main.tsx
// Self-hosted variable font — no render-blocking request to Google Fonts.
import "@fontsource-variable/jetbrains-mono"
import "./index.css"

That single import is the whole thing. Because it’s a plain module import, Vite (my bundler) sees it at build time, pulls the @font-face CSS and the .woff2 file into my own build output, and serves them from my own domain. No fonts.googleapis.com, no fonts.gstatic.com, no extra DNS lookups.

Why “variable” matters

A variable font packs a whole range of weights (light through bold) into one file, instead of shipping a separate file per weight. So @fontsource-variable/jetbrains-mono gives me every weight I might use from a single download — smaller total payload and fewer requests than grabbing four static weights.

What self-hosting actually buys you

  • No third-party round-trips. The font is served from the same origin as everything else, so it reuses the connection you already opened for your HTML and JS.
  • The bundler controls caching and hashing. Vite fingerprints the font file, so it caches aggressively and busts correctly on change.
  • Privacy and reliability. You’re not depending on Google’s servers being up or on their privacy terms.
  • It works offline / behind firewalls. Handy for intranet or PWA use.

Since my font ships inside the CSS my page already loads, I reference it through a variable in that CSS and use it everywhere the terminal aesthetic needs monospace:

css
/* the font is available as a normal family once imported */
font-family: var(--font-mono, monospace);

A note for prerendered apps

My site is statically prerendered with vite-react-ssg, and this approach fits that perfectly: the font import is resolved at build time, so the generated bundle already references a local, hashed font file. There’s no runtime request to a font CDN happening during or after hydration — the asset is just part of the site.

What to remember

  • A Google Fonts <link rel="stylesheet"> is render-blocking and adds two extra third-party domains’ worth of round-trips.
  • Self-host with Fontsource: import "@fontsource-variable/jetbrains-mono" in your entry file and let the bundler include it.
  • Prefer the variable build — one file covers all weights, so fewer/smaller requests.
  • Self-hosting means same-origin delivery, bundler-controlled caching, better privacy, and no external dependency.
  • Reference the family in CSS as usual (var(--font-mono, monospace)) once the import is in place.

A monospace font like JetBrains Mono is also central to the CRT-terminal look of the portfolio it was chosen for.

ls ./related
cat ./comments

Comments are not configured yet. Enable GitHub Discussions and paste the giscus repo-id / category-id into src/consts.ts.