Advanced (Updated: 6/2/2026)

Web Font Optimization with Claude Code: Practical Guide

Optimize web fonts with Claude Code: preload, font-display, variable fonts, subsetting, CLS/LCP, and safe prompts.

Web Font Optimization with Claude Code: Practical Guide

Font Optimization Is a Rendering Problem

Web fonts are usually discussed as a design decision, but the failure mode is performance. A slow font can hide text, swap a heading after the user has started reading, move a call-to-action button, or delay the Largest Contentful Paint element. Japanese, Chinese, Korean, and other large character sets make the risk sharper because a single font family can be much larger than the rest of the first view.

This guide shows how to use Claude Code to implement web font optimization without turning the site into a pile of hints. The goal is not to preload everything. The goal is to choose the font loading strategy, preload only critical files, use font-display deliberately, reduce font bytes with variable fonts and subsetting, decide between self-hosting and third-party delivery, and verify the result with Core Web Vitals evidence.

The official references matter here because browser behavior has details. MDN documents the font-display descriptor, and MDN’s rel="preload" guide explains the preload shape, including font crossorigin. For the broader performance model, use web.dev’s Best practices for fonts, Optimize web fonts, and Web Vitals.

Choose the Loading Strategy Before Editing

Claude Code can make code changes quickly, so the dangerous prompt is “make fonts faster.” That usually produces scattered preload tags, duplicated Google Fonts CSS, or a visual regression in the fallback stack. Start by telling Claude Code which text appears above the fold, which family is required for brand identity, and which fonts can be delayed or removed.

Use casePractical strategyWhy it worksCommon failure
Japanese or multilingual blogUse system fonts for body text, then apply a subset web font to headingsReading stays available even if the web font is lateThe display font accidentally becomes the body font
SaaS dashboardSelf-host a Latin variable font for UI labels and numbersOne file can cover many weightsUnused italics, axes, or language ranges stay in the file
Landing page heroPreload only the WOFF2 file used by the LCP headingThe conversion text appears earlyAll weights are preloaded and compete with the hero image
Legacy icon fontReplace with SVG or a component icon libraryRemoves a competing font requestPseudo-element CSS breaks silently

For a content site, the most reliable optimization is often boring: keep the body readable with system fonts and reserve the web font for high-impact headings. For a product UI, a variable font such as Inter can be a good fit because the interface may need 400, 500, 600, and 700 weights. For a landing page, the font and hero image compete for the same first-view budget, so pair this article with the Claude Code image optimization guide. For wider performance work, see performance optimization with Claude Code.

Implement Preload and Preconnect in Astro

preload is a strong browser hint. Use it for one or two font files that are definitely used in the first viewport. Do not preload every weight, every language, or fonts used only after scrolling. If you self-host fonts from your own origin, you usually do not need preconnect. If you use Google Fonts or another third-party provider, preconnect can reduce connection setup time for the CSS and font origins.

Here is a copy-pasteable Astro layout pattern. Keep the boolean explicit so a future Claude Code change does not leave third-party hints in a self-hosted setup.

---
// src/layouts/BaseLayout.astro
const criticalFonts = [
  { href: "/fonts/inter-var-latin.woff2", type: "font/woff2" },
  { href: "/fonts/noto-sans-jp-latin-kana.woff2", type: "font/woff2" },
];

const usesGoogleFonts = false;
---

<html lang="en">
  <head>
    {criticalFonts.map((font) => (
      <link rel="preload" href={font.href} as="font" type={font.type} crossorigin />
    ))}

    {usesGoogleFonts && <link rel="preconnect" href="https://fonts.googleapis.com" />}
    {usesGoogleFonts && <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />}

    <link rel="stylesheet" href="/styles/fonts.css" />
  </head>
  <body>
    <slot />
  </body>
</html>

Review the generated HTML after implementation. The href must match the file referenced by @font-face. The preload must include as="font", type="font/woff2", and crossorigin. Most importantly, the preloaded font must actually be used soon. Chrome’s warning about a preloaded resource not being used is a signal to remove the hint, not a harmless message to ignore.

Use font-display Without Creating CLS

font-display: swap is the common default because it renders text with a fallback font first and swaps to the web font when it arrives. That avoids invisible text, but it can still create Cumulative Layout Shift if the fallback font has different metrics. font-display: optional can be better for non-critical body text on slow networks because the browser may keep the fallback instead of swapping late.

The following CSS combines a Latin variable font, a Japanese subset, and fallback metric overrides. The exact size-adjust values should be measured for your font pair, but the structure is the safe starting point.

/* public/styles/fonts.css */
@font-face {
  font-family: "InterVariable";
  src: url("/fonts/inter-var-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "NotoSansJPSubset";
  src: url("/fonts/noto-sans-jp-latin-kana.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+3000-30FF, U+FF00-FFEF;
}

@font-face {
  font-family: "InterFallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

:root {
  --font-ui: "InterVariable", "InterFallback", system-ui, sans-serif;
  --font-ja: "NotoSansJPSubset", "Hiragino Sans", "Yu Gothic", sans-serif;
}

body {
  font-family: var(--font-ui);
}

article {
  font-family: var(--font-ja);
}

MDN documents unicode-range, which lets a font face cover only part of the character space. That does not magically reduce the file. It tells the browser when the face applies. You still need a smaller WOFF2 file if you want fewer bytes.

Reduce Bytes with Variable Fonts and Subsetting

Variable fonts can replace multiple static files when you need several weights. They are not automatically smaller. A variable font with many axes and broad language coverage can still be heavy. Ask Claude Code to inspect which weights and scripts are actually used before switching.

Subsetting is the more aggressive move: create a smaller font file that contains only the characters you need. It works well for navigation labels, hero headings, and Latin or kana ranges. It is riskier for article body text because new content may introduce characters that are not in the subset. Treat body subsetting as a build process, not a one-time manual export.

# Create a WOFF2 subset with Latin, punctuation, kana, and full-width forms.
python -m pip install "fonttools[woff]"
mkdir -p public/fonts

pyftsubset ./vendor-fonts/NotoSansJP-Regular.ttf \
  --output-file=./public/fonts/noto-sans-jp-latin-kana.woff2 \
  --flavor=woff2 \
  --layout-features='*' \
  --unicodes="U+0000-00FF,U+3000-30FF,U+FF00-FFEF"

This command intentionally excludes most kanji. That is fine for a header and navigation subset, but it is not enough for a Japanese article body. If you want body text coverage, extract characters from your MDX files and feed them to pyftsubset with a text file, or create a separate kanji subset. Also verify the font license before self-hosting or modifying a font file.

Decide Between Self-Hosted and Third-Party Fonts

Self-hosting gives you predictable URLs, long cache headers, exact preload targets, and full control over subsetting. It also makes you responsible for license compliance, updates, and build scripts. Third-party delivery, such as Google Fonts, is convenient and often well maintained, but it introduces external connections and an extra CSS request before the font file.

Decision pointSelf-hosted fontsThird-party fonts
Connection setupSame originDNS, TLS, CSS origin, font origin
Preload targetExact WOFF2 fileHarder if CSS decides the URL
CachingYou control headers and filenamesProvider controls headers
SubsettingFully customizableDepends on provider features
OperationsMore responsibilityEasier setup, more dependency

For most performance-sensitive pages, self-host critical first-view fonts and delay decorative fonts. For quick prototypes, third-party fonts are acceptable if you use display=swap, preconnect only the required origins, and avoid loading unused weights. Never leave both the provider CSS and self-hosted CSS active at the same time.

Verify with Lighthouse and a WebPageTest-Style Waterfall

After implementation, do not trust the visual result alone. Run Lighthouse for a lab signal, then inspect the network waterfall like WebPageTest: HTML, CSS, critical font, hero image, JavaScript. You want the critical font to start early without delaying the LCP image or the CSS needed to render the page.

URL="https://example.com/"
npx --yes lighthouse "$URL" \
  --only-categories=performance \
  --chrome-flags="--headless" \
  --output=json \
  --output-path=./lighthouse-fonts.json

node -e "const r=require('./lighthouse-fonts.json'); for (const id of ['largest-contentful-paint','cumulative-layout-shift','font-display']) console.log(id, r.audits[id]?.displayValue ?? r.audits[id]?.score ?? 'n/a')"

Use this checklist in the browser or WebPageTest waterfall:

CheckGood signalBad signal
Font countOne or two critical WOFF2 files earlyMany weights start before CSS settles
Preload usagePreloaded font is used in the first viewport”Preloaded but not used” warning
CLSHeading and CTA keep their size during swapButton or heading jumps after font load
LCPFont does not delay the hero text or imageFont request blocks the LCP element
Repeat viewCache removes font transfer costFingerprints or headers prevent caching

Common failures are concrete. A bold font is preloaded but the first view only uses regular text. Google Fonts CSS remains after self-hosting, so the browser downloads two font families. font-display: swap is added, but fallback metrics are not adjusted and the CTA moves. A Japanese subset excludes long vowels, full-width digits, or punctuation. These are exactly the bugs Claude Code can miss unless the acceptance criteria name them.

Claude Code Prompts for Safe Implementation

Split the work into audit, implementation, and verification. Start with a read-only audit.

Audit web font loading in this Astro site. Do not edit files yet.

Find:
- @font-face, Google Fonts, Fontsource, and CSS import locations
- Font files used above the fold
- preload and preconnect hints that are missing or unnecessary
- CLS or LCP risks caused by font swapping
- Candidates for self-hosting, variable fonts, and subsetting

Return:
- A prioritized table of changes
- Files that should be edited and files that must not be touched
- Verification commands and residual risks

Then ask for a narrow implementation.

Implement web font optimization only in these files:
- src/layouts/BaseLayout.astro
- public/styles/fonts.css
- generated files under public/fonts/

Acceptance criteria:
- Preload only WOFF2 files used in the first viewport
- Do not add preconnect when fonts are self-hosted
- Every @font-face has a deliberate font-display value
- Fallback metrics are adjusted to reduce CLS
- Existing routes, article slugs, hero images, and unrelated content are untouched

Verification:
- npm run build
- node scripts/check-code-fences.mjs
- Lighthouse check for LCP, CLS, and font-display
- Report what could not be verified

The prompt is deliberately strict because font optimization touches layout, network priority, build output, and sometimes licensing. Claude Code should be asked to prove the result, not just change CSS.

Practical Result and CTA

In Masa’s workflow, the biggest improvement came from narrowing the task before editing. “Optimize fonts” produced noisy diffs. “Preload only the font used by the LCP heading, keep body text readable with system fonts, and report CLS risk” produced smaller and safer changes. On a Japanese article page, a Latin and kana subset made the first view feel steadier, but body coverage still required a separate character extraction process. The final check that mattered most was a slow-mobile waterfall plus a manual view of the heading and CTA during font swap.

If you are building a team workflow, put the font rules in CLAUDE.md so the same mistakes do not return in the next redesign. Start with the free cheat sheet for safe Claude Code commands, use the product templates when you want reusable prompts and review checklists, and consider Claude Code training when Core Web Vitals, article quality, and monetization CTAs need to be improved together.

Self-check before publication: this article includes more than three concrete use cases, copy-pasteable Astro/CSS/bash examples, official MDN and web.dev links, internal links, specific pitfalls, a natural CTA, and a hands-on verification note.

#Claude Code #Web Fonts #Core Web Vitals #Astro #performance
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.