Actionable rules for selecting, preparing, delivering, and maintaining performant web fonts in modern front-end stacks.
Your site loads in under 2 seconds, but your fonts take 4 seconds to render. Users see invisible text, then jarring layout shifts when your custom fonts finally load. You're shipping 400KB of fonts when you need 80KB. Sound familiar?
Every additional font family, weight, and style compounds your performance debt. Developers often:
The result? Poor Core Web Vitals, frustrated users, and wasted bandwidth.
These Cursor Rules implement a complete font optimization strategy that eliminates guesswork. You get automated subsetting, efficient delivery patterns, and performance monitoring built into your development workflow.
Instead of manually optimizing fonts for each project, you follow proven patterns that:
font-display and fallback strategies@font-face patterns across projects/* Inefficient: Multiple formats, no subsetting, blocking load */
@font-face {
font-family: 'CustomFont';
src: url('font.ttf'); /* 180KB, poor compression */
/* No font-display, causes FOIT */
/* No unicode-range, loads all scripts */
}
/* Efficient: WOFF2 first, subset, swap loading */
@font-face {
font-family: "Acme";
font-style: normal;
font-weight: 400;
font-display: swap; /* Prevents FOIT */
src: url("/fonts/acme-v17-latin-400.woff2") format("woff2"),
url("/fonts/acme-v17-latin-400.woff") format("woff");
unicode-range: U+000-5FF; /* Latin only: 60KB → 15KB */
}
// Automatic optimization with next/font
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
weight: ['400', '600'], // Only load needed weights
display: 'swap',
variable: '--font-inter'
});
export default function Layout({ children }) {
return (
<html className={inter.variable}>
<body>{children}</body>
</html>
);
}
// Automated font performance validation
import { load } from 'fontfaceobserver';
async function validateFontPerformance() {
try {
await load('Acme', { weight: 400, timeout: 2000 });
document.documentElement.classList.add('font-acme-loaded');
} catch {
// Graceful degradation to system fonts
document.documentElement.classList.add('font-fallback');
}
}
npm run font-audit to see total payloadfont-display: swapFor Next.js projects:
@font-face with next/font importsFor NitroPack projects:
These rules transform font optimization from a manual, error-prone process into a systematic approach that delivers measurable performance gains while maintaining design fidelity. Your fonts load faster, your users see content immediately, and your development workflow becomes predictable and maintainable.
You are an expert in Web Performance, CSS/HTML5, JavaScript (ES2023), Build Tooling (Webpack, Vite, Rollup), Next.js, and NitroPack.
Key Principles
- Ship the smallest possible font payload, in the most efficient format, as late as possible without harming UX.
- Treat fonts as critical assets: audit, measure, and budget their total (compressed) KB.
- Prefer progressive enhancement: visible fallback text immediately, swapped-in branded font later.
- Automate: integrate subsetting, compression, and preloading into the build pipeline to eliminate manual steps.
- Document every added font (family, weight, style, unicode-range) in a single manifest to avoid drift.
CSS / HTML Rules
- Always provide fonts in this order of preference: WOFF2 (required) ➜ WOFF (fallback) ➜ system-font (final fallback). De-prioritize TTF/EOT unless targeting legacy IE8.
- Use a dedicated @font-face block per weight/style. Keep descriptors minimal:
```css
/* src order: WOFF2 first, then WOFF */
@font-face {
font-family: "Acme";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/acme-v17-latin-400.woff2") format("woff2"),
url("/fonts/acme-v17-latin-400.woff") format("woff");
unicode-range: U+000-5FF; /* Latin subset */
}
```
- Never embed base64 fonts in CSS; they kill caching and inflate CSS.
- Apply `font-display: swap` for all custom fonts to avoid FOIT (flash of invisible text). Use `optional` for low-priority fonts.
- Preload only above-the-fold fonts:
```html
<link rel="preload" href="/fonts/acme-v17-latin-400.woff2" as="font" type="font/woff2" crossorigin>
```
- Host fonts on the same origin (preferred) or configure CORS with `Access-Control-Allow-Origin: *`.
- Add Brotli + GZIP to all font responses; verify content-encoding with dev-tools.
- Cache fonts for 1 year and allow immutable: `Cache-Control: public, max-age=31536000, immutable`.
- When using variable fonts, include only axes actually used (e.g., `wght`, `ital`) and clamp min/max via `font-variation-settings`.
JavaScript Rules (Loader Helper)
- Do not lazy-load fonts via JS unless critical path is already CPA-compressed; prefer native CSS.
- When JS is required (e.g., FontFace API for A/B testing fonts):
```js
import {load} from 'fontfaceobserver';
await load('Acme', {weight: 400, timeout: 2000});
document.documentElement.classList.add('font-acme-loaded');
```
- Always set a 2-3 s timeout; fail fast to fallback.
Error Handling & Validation
- Return early on loading failure: apply `.font-fallback` class to disable future observers.
- Log missing glyph events by comparing rendered string width vs. expected width for validation.
- Configure server to return 410 for deleted font files so broken `src` are caught during CI.
- In CI, lint CSS for duplicate family names + overlapping unicode-range with `stylelint-font-family-no-dup`.
Framework-Specific Rules
Next.js (>=13 with `next/font`)
- Use built-in font optimization over manual `@font-face` when possible:
```tsx
import { Acme } from 'next/font/google';
const acme = Acme({subsets: ['latin'], weight: ['400'], display: 'swap'});
```
- Remove any `<link rel="preload">` duplicates; Next.js auto-inlines preload.
- Place `next/font` imports in layout.tsx to avoid re-evaluations on every page.
NitroPack
- Enable “Font Optimization” → “Defer Non-Critical Fonts”.
- White-list critical fonts in NitroPack settings; others are deferred automatically.
- Verify generated critical CSS includes only base64-inlined glyph subset <3 KB.
Additional Sections
Performance
- Budget: ≤100 KB total compressed font payload on first load.
- Track CLS: ensure fallback font metrics match custom font (use Kevin Powell’s Fallback Font Generator).
- Use Lighthouse Audit → “Preload key requests” & “Properly size text” to validate.
Testing
- Write Playwright test that throttles network (Slow 3G) and asserts `.font-acme-loaded` class within 3 s.
- Snapshot Font Performance in WebPageTest; diff TTFB + Start Render after changes.
Security
- Serve fonts over HTTPS only; block mixed content.
- Validate MIME types on the server: `font/woff2`, `font/woff`.
Accessibility & Internationalization
- Always include `unicode-range` subsets for each script you support (e.g., Latin, Cyrillic, Greek) and fall back to system font for unsupported glyphs.
- Avoid using icon fonts; prefer SVG with `role="img"` for assistive technology.
Common Pitfalls
- Multiple `@font-face` blocks pointing to the same file – duplicates downloads due to query strings.
- Forgetting to update `src` hash when using SRI after regenerating fonts.
- Using `font-weight: normal` (400) when only 300 & 600 subsets are shipped leads browsers to synthesize, hurting rendering quality.
Repository Checklist (copy into PR template)
- [ ] Added font to `/fonts` with hashed filename.
- [ ] Added matching @font-face block with `font-display: swap`.
- [ ] Added preload tag or declared via `next/font`.
- [ ] Updated font manifest (`fonts.json`).
- [ ] Ran `npm run font-audit` and stayed under budget.