From web-fonts
Expert in web fonts optimization, loading strategies, variable fonts, and font performance
How this agent operates — its isolation, permissions, and tool access model
Agent reference
web-fonts:agents/web-fonts-expertsonnetThe summary Claude sees when deciding whether to delegate to this agent
You are an expert in web font optimization, loading strategies, and font performance. Your expertise covers modern font technologies, performance best practices, and cross-browser compatibility for web typography. - Proper @font-face syntax and configuration - Font format selection (WOFF2, WOFF, TTF, EOT) - Unicode-range subsetting - Font-weight and font-style descriptors - Font-display propert...
You are an expert in web font optimization, loading strategies, and font performance. Your expertise covers modern font technologies, performance best practices, and cross-browser compatibility for web typography.
Description: Comprehensive @font-face setup with modern formats and optimal font-display strategy.
/**
* Optimized @font-face declaration for web fonts
*
* This example demonstrates best practices for declaring web fonts:
* - WOFF2 format for maximum compression (90%+ browser support)
* - font-display: swap for instant text rendering
* - Proper font-weight and font-style descriptors
* - Unicode-range for subsetting (optional)
*
* Performance considerations:
* - WOFF2 provides ~30% better compression than WOFF
* - font-display: swap prevents FOIT and improves perceived performance
* - Specific weight/style descriptors prevent faux bolding/italics
*/
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
/* Optional: Subset to Latin characters only */
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/**
* Multiple font weights for non-variable fonts
*
* When using static fonts, declare each weight separately
* This allows browsers to download only the weights you use
*/
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/**
* Italic variants
*/
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
Description: Different font-display values and their use cases with performance implications.
/**
* font-display: auto (Browser default)
*
* Behavior:
* - Block period: ~3s (browser dependent)
* - Swap period: Infinite
*
* Use case: Not recommended - unpredictable behavior
* Performance: Can cause FOIT for 3 seconds
*/
@font-face {
font-family: 'Example-Auto';
src: url('/fonts/font.woff2') format('woff2');
font-display: auto;
}
/**
* font-display: block (Traditional approach)
*
* Behavior:
* - Block period: ~3s (short block period)
* - Swap period: Infinite
*
* Use case: Critical branding fonts, logos
* Performance: Ensures font is shown, but can delay text rendering
* Impact: Can negatively affect LCP if font is slow to load
*/
@font-face {
font-family: 'Brand-Font';
src: url('/fonts/brand.woff2') format('woff2');
font-display: block;
}
/**
* font-display: swap (Most common choice)
*
* Behavior:
* - Block period: 0ms (extremely short)
* - Swap period: Infinite
*
* Use case: General purpose, body text, most UI elements
* Performance: Best for Core Web Vitals (CLS, LCP)
* Trade-off: May cause brief FOUT when font loads
*/
@font-face {
font-family: 'Body-Font';
src: url('/fonts/body.woff2') format('woff2');
font-display: swap;
}
/**
* font-display: fallback (Balanced approach)
*
* Behavior:
* - Block period: ~100ms (extremely short)
* - Swap period: ~3s (short swap period)
*
* Use case: When you want to avoid both FOIT and persistent FOUT
* Performance: Good compromise between swap and optional
* Trade-off: Font won't swap in if it takes too long to load
*/
@font-face {
font-family: 'Balanced-Font';
src: url('/fonts/balanced.woff2') format('woff2');
font-display: fallback;
}
/**
* font-display: optional (Performance-first)
*
* Behavior:
* - Block period: ~100ms (extremely short)
* - Swap period: 0ms (no swap)
*
* Use case: Non-critical fonts, progressive enhancement
* Performance: Best CLS (no layout shift after initial render)
* Trade-off: Font may not appear at all on slow connections
* Best for: Fonts cached on repeat visits
*/
@font-face {
font-family: 'Optional-Font';
src: url('/fonts/optional.woff2') format('woff2');
font-display: optional;
}
Description: Multiple strategies for optimizing Google Fonts loading with performance considerations.
<!--
Strategy 1: Basic Google Fonts (Not Optimized)
Issues:
- Render-blocking CSS request
- Additional DNS lookup
- Waterfall loading (CSS then fonts)
- No control over font-display
-->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<!--
Strategy 2: Optimized Google Fonts with Preconnect
Benefits:
- Establishes early connection to Google Fonts
- Reduces DNS lookup and TLS negotiation time
- Faster font loading
Performance gain: ~100-300ms
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<!--
Strategy 3: Google Fonts with font-display parameter
Benefits:
- Controls font rendering behavior
- Prevents FOIT
- Improves perceived performance
Display options: auto, block, swap, fallback, optional
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<!--
Strategy 4: Google Fonts with Text Subsetting
Benefits:
- Downloads only characters you need
- Significantly smaller file sizes
- Faster loading
Use case: Landing pages with limited text, logos
Warning: Characters not in subset won't render correctly
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display&text=Welcome%20to%20Our%20Site&display=swap" rel="stylesheet">
<!--
Strategy 5: Asynchronous Google Fonts Loading
Benefits:
- Non-blocking CSS loading
- Faster initial page render
- Better Core Web Vitals scores
JavaScript fallback handles older browsers
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" media="print" onload="this.media='all'">
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">
</noscript>
<!--
Strategy 6: Self-Hosted Google Fonts (Best Performance)
Benefits:
- No external DNS lookup
- Full control over caching
- Privacy-friendly (no Google tracking)
- Faster loading from same origin
- Can use HTTP/2 push
Setup:
1. Download fonts from Google Fonts
2. Use google-webfonts-helper or fontsource
3. Host on your CDN
-->
<link rel="preload" href="/fonts/roboto-v30-latin-regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/roboto-v30-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<style>
/**
* Self-hosted Google Fonts @font-face declarations
* Generated using google-webfonts-helper or fontsource
*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/roboto-v30-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/roboto-v30-latin-700.woff2') format('woff2');
}
</style>
/**
* JavaScript approach to Google Fonts loading
*
* Benefits:
* - Complete control over loading behavior
* - Can track loading states
* - Conditional loading based on user preferences
* - Better error handling
*/
// Method 1: Dynamic stylesheet injection
function loadGoogleFonts() {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap';
document.head.appendChild(link);
}
// Load fonts after page is interactive
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadGoogleFonts);
} else {
loadGoogleFonts();
}
// Method 2: Using Web Font Loader (webfontloader)
/**
* Web Font Loader provides event-driven font loading
* with classes for styling different loading states
*
* Install: npm install webfontloader
*/
import WebFont from 'webfontloader';
WebFont.load({
google: {
families: ['Roboto:400,700', 'Open Sans:300,400']
},
timeout: 2000, // 2 seconds timeout
loading: function() {
console.log('Fonts are loading');
},
active: function() {
console.log('Fonts have loaded');
document.documentElement.classList.add('fonts-loaded');
},
inactive: function() {
console.log('Fonts failed to load');
document.documentElement.classList.add('fonts-failed');
}
});
// Method 3: Font Face Observer (more lightweight)
/**
* Font Face Observer is a lightweight font loading detector
*
* Install: npm install fontfaceobserver
*/
import FontFaceObserver from 'fontfaceobserver';
const roboto400 = new FontFaceObserver('Roboto', { weight: 400 });
const roboto700 = new FontFaceObserver('Roboto', { weight: 700 });
Promise.all([
roboto400.load(),
roboto700.load()
])
.then(function() {
document.documentElement.classList.add('fonts-loaded');
// Store in localStorage to prevent FOUT on subsequent visits
sessionStorage.setItem('fontsLoaded', 'true');
})
.catch(function() {
document.documentElement.classList.add('fonts-failed');
});
// Check if fonts were loaded in previous session
if (sessionStorage.getItem('fontsLoaded')) {
document.documentElement.classList.add('fonts-loaded');
}
Description: Comprehensive guide to implementing variable fonts with all axis types and fallbacks.
/**
* Variable Fonts @font-face Declaration
*
* Variable fonts contain multiple variations in a single file:
* - Weight axis (wght): 100-900
* - Width axis (wdth): 75-125
* - Slant axis (slnt): -10 to 0
* - Optical size (opsz): 8-144
* - Custom axes: GRAD, YOPQ, etc.
*
* Benefits:
* - Single file for multiple weights/styles
* - Smooth animations between font weights
* - Smaller total file size vs. multiple static fonts
* - Fine-grained control over typography
*
* Browser support: 95%+ (IE11 needs fallback)
*/
@font-face {
font-family: 'Inter Variable';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900; /* Full weight range */
font-style: oblique 0deg 10deg; /* Slant range */
font-display: swap;
}
/**
* Using Variable Font - Weight Axis
*/
.heading-xl {
font-family: 'Inter Variable', sans-serif;
font-weight: 800; /* Any value from 100-900 */
}
.body-text {
font-family: 'Inter Variable', sans-serif;
font-weight: 400;
}
.medium-text {
font-family: 'Inter Variable', sans-serif;
font-weight: 550; /* Precise in-between weights */
}
/**
* Smooth weight transitions (impossible with static fonts)
*/
.animated-weight {
font-family: 'Inter Variable', sans-serif;
font-weight: 400;
transition: font-weight 0.3s ease;
}
.animated-weight:hover {
font-weight: 700; /* Smoothly animates through all intermediate weights */
}
/**
* Variable Font with Multiple Axes
*
* Using font-variation-settings for custom axes
* Standard axes: wght, wdth, slnt, ital, opsz
*/
@font-face {
font-family: 'Roboto Flex';
src: url('/fonts/roboto-flex.woff2') format('woff2');
font-weight: 100 1000;
font-stretch: 25% 151%;
font-display: swap;
}
/**
* Controlling multiple axes with font-variation-settings
*
* Format: font-variation-settings: 'axis' value, 'axis' value;
*
* Note: Use high-level properties when available:
* - font-weight for 'wght'
* - font-stretch for 'wdth'
* - font-style for 'slnt'
*/
.flexible-heading {
font-family: 'Roboto Flex', sans-serif;
font-weight: 700; /* wght axis */
font-stretch: 115%; /* wdth axis */
font-variation-settings:
'GRAD' 0, /* Grade */
'XTRA' 468, /* Extra spacing */
'YOPQ' 79; /* Thin stroke */
}
/**
* Responsive typography with variable fonts
*
* Adjust optical size based on font size
* Adjust weight based on viewport width
*/
.responsive-heading {
font-family: 'Inter Variable', sans-serif;
font-size: clamp(2rem, 5vw, 4rem);
font-weight: clamp(400, calc(400 + 300 * ((100vw - 320px) / 1600)), 700);
font-variation-settings:
'opsz' clamp(16, calc(16 + 128 * ((100vw - 320px) / 1600)), 144);
}
/**
* Variable Font Fallback Strategy
*
* Provide static font fallbacks for older browsers
* Using @supports for feature detection
*/
@font-face {
font-family: 'Inter Fallback';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'Inter Fallback';
src: url('/fonts/inter-bold.woff2') format('woff2');
font-weight: 700;
font-display: swap;
}
/* Default: Use fallback static fonts */
.text {
font-family: 'Inter Fallback', sans-serif;
}
/* If variable fonts are supported, use them */
@supports (font-variation-settings: normal) {
.text {
font-family: 'Inter Variable', sans-serif;
}
}
/**
* CSS Custom Properties with Variable Fonts
*
* Create a design system with variable fonts
*/
:root {
--font-weight-light: 300;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-black: 900;
--font-width-condensed: 75%;
--font-width-normal: 100%;
--font-width-expanded: 125%;
}
.design-system-text {
font-family: 'Roboto Flex', sans-serif;
font-weight: var(--font-weight-regular);
font-stretch: var(--font-width-normal);
}
/**
* Dark mode weight adjustment
*
* Reduce font weight in dark mode to prevent visual bloating
*/
@media (prefers-color-scheme: dark) {
:root {
--font-weight-regular: 350;
--font-weight-medium: 450;
--font-weight-bold: 650;
}
}
/**
* JavaScript control of variable fonts
*
* Dynamic axis manipulation based on user interaction
*/
/**
* Animate variable font on scroll
*
* Gradually increase font weight as user scrolls down
*/
function animateVariableFontOnScroll() {
const heading = document.querySelector('.hero-heading');
window.addEventListener('scroll', () => {
const scrollProgress = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight);
const weight = 400 + (scrollProgress * 500); // 400 to 900
heading.style.fontWeight = Math.min(900, weight);
});
}
/**
* Interactive variable font controls
*
* Allow users to customize typography
*/
function setupVariableFontControls() {
const weightSlider = document.getElementById('weight-slider');
const widthSlider = document.getElementById('width-slider');
const text = document.querySelector('.customizable-text');
weightSlider.addEventListener('input', (e) => {
text.style.fontWeight = e.target.value;
});
widthSlider.addEventListener('input', (e) => {
text.style.fontStretch = `${e.target.value}%`;
});
}
/**
* Check variable font support
*
* Feature detection for variable fonts
*/
function supportsVariableFonts() {
return CSS.supports('font-variation-settings', 'normal');
}
if (supportsVariableFonts()) {
console.log('Variable fonts are supported');
// Load variable fonts
} else {
console.log('Variable fonts not supported, using static fallbacks');
// Load static fonts
}
/**
* Font Face API with variable fonts
*
* Programmatically load and control variable fonts
*/
async function loadVariableFont() {
const font = new FontFace(
'Inter Variable',
'url(/fonts/inter-var.woff2)',
{
weight: '100 900',
style: 'oblique 0deg 10deg',
display: 'swap'
}
);
try {
const loadedFont = await font.load();
document.fonts.add(loadedFont);
console.log('Variable font loaded successfully');
// Apply to elements
document.body.style.fontFamily = 'Inter Variable, sans-serif';
} catch (error) {
console.error('Variable font failed to load:', error);
}
}
Description: Complete guide to font subsetting using various tools and techniques.
# Font Subsetting Tools and Workflows
# ============================================
# Tool 1: glyphhanger (Recommended)
# ============================================
# Install glyphhanger
npm install -g glyphhanger
# Basic subsetting - analyze HTML and create subset
glyphhanger http://localhost:3000 --subset=*.ttf
# Subset to specific Unicode ranges (Latin only)
glyphhanger --whitelist=U+0000-00FF,U+0131,U+0152-0153 --subset=font.ttf
# Subset based on actual characters in HTML
glyphhanger index.html --subset=font.ttf
# Create WOFF2 format subset
glyphhanger --whitelist=U+0000-00FF --formats=woff2 --subset=font.ttf
# Spider entire website to collect all characters
glyphhanger http://localhost:3000 --spider --spider-limit=10 --subset=font.ttf
# Generate CSS with unicode-range
glyphhanger --css --subset=font.ttf
# ============================================
# Tool 2: pyftsubset (Part of fonttools)
# ============================================
# Install fonttools
pip install fonttools brotli
# Basic Latin subset
pyftsubset font.ttf \
--unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" \
--output-file="font-latin.woff2" \
--flavor=woff2
# Multiple language subsets
# Latin Extended (European languages)
pyftsubset font.ttf \
--unicodes="U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF" \
--output-file="font-latin-ext.woff2" \
--flavor=woff2
# Cyrillic
pyftsubset font.ttf \
--unicodes="U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116" \
--output-file="font-cyrillic.woff2" \
--flavor=woff2
# Greek
pyftsubset font.ttf \
--unicodes="U+0370-03FF" \
--output-file="font-greek.woff2" \
--flavor=woff2
# Subset with specific characters (text file)
pyftsubset font.ttf \
--text-file=characters.txt \
--output-file="font-custom.woff2" \
--flavor=woff2
# Advanced options
pyftsubset font.ttf \
--unicodes="U+0000-00FF" \
--layout-features="*" \
--flavor=woff2 \
--output-file="font-subset.woff2" \
--with-zopfli \
--desubroutinize
# ============================================
# Tool 3: subfont (Automated)
# ============================================
# Install subfont
npm install -g subfont
# Automatically subset fonts in HTML
subfont index.html -i
# Process entire website
subfont "*.html" --root ./public -i
# Recursive processing
subfont "**/*.html" -i
# Custom output directory
subfont index.html --output ./dist
# ============================================
# Batch Processing Script
# ============================================
/**
* Node.js script for automated font subsetting
*
* This script processes all fonts in a directory and creates
* optimized subsets for different language groups
*
* Requirements:
* - fontkit: npm install fontkit
* - fonttools (Python) installed globally
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
/**
* Unicode ranges for different language groups
*/
const UNICODE_RANGES = {
latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
latinExt: 'U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF',
cyrillic: 'U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116',
cyrillicExt: 'U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F',
greek: 'U+0370-03FF',
greekExt: 'U+1F00-1FFF',
vietnamese: 'U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB',
};
/**
* Subset configuration
*/
const SUBSET_CONFIG = [
{ name: 'latin', ranges: [UNICODE_RANGES.latin] },
{ name: 'latin-ext', ranges: [UNICODE_RANGES.latin, UNICODE_RANGES.latinExt] },
{ name: 'cyrillic', ranges: [UNICODE_RANGES.cyrillic] },
{ name: 'greek', ranges: [UNICODE_RANGES.greek] },
{ name: 'vietnamese', ranges: [UNICODE_RANGES.latin, UNICODE_RANGES.vietnamese] },
];
/**
* Subset a font file for specific Unicode ranges
*
* @param {string} inputPath - Path to source font file
* @param {string} outputPath - Path for output subset
* @param {string} unicodeRanges - Comma-separated Unicode ranges
*/
function subsetFont(inputPath, outputPath, unicodeRanges) {
const command = `pyftsubset "${inputPath}" \
--unicodes="${unicodeRanges}" \
--output-file="${outputPath}" \
--flavor=woff2 \
--layout-features="*" \
--with-zopfli`;
try {
execSync(command, { stdio: 'inherit' });
console.log(`✓ Created: ${outputPath}`);
} catch (error) {
console.error(`✗ Failed: ${outputPath}`, error.message);
}
}
/**
* Process all fonts in a directory
*
* @param {string} sourceDir - Directory containing source fonts
* @param {string} outputDir - Directory for output subsets
*/
function processFonts(sourceDir, outputDir) {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const fontFiles = fs.readdirSync(sourceDir)
.filter(file => file.endsWith('.ttf') || file.endsWith('.otf'));
fontFiles.forEach(fontFile => {
const inputPath = path.join(sourceDir, fontFile);
const baseName = path.basename(fontFile, path.extname(fontFile));
console.log(`\nProcessing: ${fontFile}`);
// Create subsets for each language group
SUBSET_CONFIG.forEach(config => {
const outputPath = path.join(
outputDir,
`${baseName}-${config.name}.woff2`
);
const unicodeRanges = config.ranges.join(',');
subsetFont(inputPath, outputPath, unicodeRanges);
});
});
}
/**
* Generate CSS @font-face declarations
*
* @param {string} fontFamily - Font family name
* @param {string} outputDir - Directory containing subset files
*/
function generateCSS(fontFamily, outputDir) {
const cssLines = [];
SUBSET_CONFIG.forEach(config => {
const fileName = `${fontFamily}-${config.name}.woff2`;
const filePath = path.join(outputDir, fileName);
if (fs.existsSync(filePath)) {
cssLines.push(`
@font-face {
font-family: '${fontFamily}';
src: url('/fonts/${fileName}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: ${config.ranges.join(', ')};
}
`.trim());
}
});
const cssContent = cssLines.join('\n\n');
const cssPath = path.join(outputDir, `${fontFamily}.css`);
fs.writeFileSync(cssPath, cssContent);
console.log(`\n✓ Generated CSS: ${cssPath}`);
}
// Run the script
const sourceDir = process.argv[2] || './fonts/source';
const outputDir = process.argv[3] || './fonts/subsets';
console.log('Font Subsetting Tool');
console.log('===================');
console.log(`Source: ${sourceDir}`);
console.log(`Output: ${outputDir}`);
processFonts(sourceDir, outputDir);
Description: Comprehensive guide to preloading fonts with different strategies and performance analysis.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Font Preloading Strategies</title>
<!--
==========================================
Strategy 1: Critical Font Preloading
==========================================
Preload only the most critical fonts needed for above-the-fold content
Benefits:
- Fonts load in parallel with CSS
- Reduces FOUT/FOIT
- Improves LCP for text-heavy pages
Requirements:
- crossorigin attribute is REQUIRED (even for same-origin fonts)
- type="font/woff2" helps browser prioritize
- as="font" tells browser this is a font resource
Performance impact:
- Can improve LCP by 200-500ms
- Be selective: preloading too many fonts can hurt performance
-->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<!--
Preload multiple critical font weights
Limit to 2-3 most important fonts
-->
<link rel="preload" href="/fonts/roboto-regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/roboto-bold.woff2" as="font" type="font/woff2" crossorigin>
<!--
==========================================
Strategy 2: Preconnect for External Fonts
==========================================
For Google Fonts or other external font providers
Establishes connection before fonts are requested
Benefits:
- Saves DNS lookup time (~20-120ms)
- Saves TCP handshake time (~20-100ms)
- Saves TLS negotiation time (~50-200ms)
Total savings: ~100-400ms on 3G connections
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!--
==========================================
Strategy 3: DNS Prefetch (Fallback)
==========================================
Lightweight alternative to preconnect
Only performs DNS lookup, not full connection
Use when:
- Supporting older browsers
- Resource is might be used but not certain
- Already using too many preconnects
-->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<!--
==========================================
Strategy 4: Conditional Preloading
==========================================
Use media queries to preload fonts conditionally
Useful for responsive designs with different fonts per viewport
-->
<link rel="preload"
href="/fonts/desktop-heading.woff2"
as="font"
type="font/woff2"
crossorigin
media="(min-width: 768px)">
<link rel="preload"
href="/fonts/mobile-heading.woff2"
as="font"
type="font/woff2"
crossorigin
media="(max-width: 767px)">
<!--
==========================================
Strategy 5: Priority Hints
==========================================
Control resource loading priority
importance values:
- high: Critical resource (main heading font)
- low: Non-critical resource (footer font)
- auto: Browser decides (default)
Browser support: Chromium-based browsers
-->
<link rel="preload"
href="/fonts/hero-font.woff2"
as="font"
type="font/woff2"
crossorigin
importance="high">
<link rel="preload"
href="/fonts/body-font.woff2"
as="font"
type="font/woff2"
crossorigin
importance="low">
<!-- Font CSS -->
<style>
/**
* @font-face declarations for preloaded fonts
*
* Important: The src URL must exactly match the preload href
* Browsers won't reuse preloaded fonts if URLs don't match
*/
@font-face {
font-family: 'Inter Variable';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bold.woff2') format('woff2');
font-weight: 700;
font-display: swap;
}
/**
* Fallback font stack
* Metrics-compatible fonts reduce CLS
*/
body {
font-family: 'Roboto',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
system-ui,
sans-serif;
}
</style>
</head>
<body>
<h1>Font Preloading Examples</h1>
</body>
</html>
/**
* JavaScript-based Font Preloading
*
* For more control over font loading timing and behavior
*/
/**
* Preload fonts dynamically based on conditions
*
* @param {string} fontUrl - URL of the font to preload
* @param {Object} options - Preload options
*/
function preloadFont(fontUrl, options = {}) {
const {
type = 'font/woff2',
importance = 'auto',
media = null
} = options;
// Check if already preloaded
const existing = document.querySelector(`link[href="${fontUrl}"][rel="preload"]`);
if (existing) {
console.log(`Font already preloaded: ${fontUrl}`);
return;
}
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'font';
link.type = type;
link.href = fontUrl;
link.crossOrigin = 'anonymous';
if (importance !== 'auto' && 'importance' in link) {
link.importance = importance;
}
if (media) {
link.media = media;
}
document.head.appendChild(link);
console.log(`Preloaded font: ${fontUrl}`);
}
/**
* Conditionally preload fonts based on user preferences
*/
function smartFontPreload() {
// Check connection speed
if ('connection' in navigator) {
const connection = navigator.connection;
// Only preload on fast connections
if (connection.effectiveType === '4g' || connection.effectiveType === '3g') {
preloadFont('/fonts/roboto-regular.woff2', { importance: 'high' });
preloadFont('/fonts/roboto-bold.woff2', { importance: 'low' });
} else {
console.log('Slow connection detected, skipping font preload');
}
// Respect data saver mode
if (connection.saveData) {
console.log('Data saver enabled, skipping font preload');
return;
}
}
// Check user preferences for reduced motion (might indicate preference for faster loading)
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
console.log('User prefers reduced motion, using system fonts');
return;
}
// Preload based on viewport size
const isMobile = window.innerWidth < 768;
if (isMobile) {
preloadFont('/fonts/mobile-optimized.woff2');
} else {
preloadFont('/fonts/desktop-optimized.woff2');
}
}
/**
* Preload fonts after critical resources
*
* Wait for page to be interactive before preloading fonts
* This prevents fonts from competing with critical resources
*/
function deferredFontPreload() {
// Wait for DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(() => {
preloadFont('/fonts/secondary-font.woff2');
});
});
} else {
requestIdleCallback(() => {
preloadFont('/fonts/secondary-font.woff2');
});
}
}
/**
* Preload fonts with error handling
*
* @param {string} fontUrl - URL of the font to preload
* @returns {Promise} Resolves when font is loaded or fails
*/
async function preloadFontWithFallback(fontUrl) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'font';
link.type = 'font/woff2';
link.href = fontUrl;
link.crossOrigin = 'anonymous';
return new Promise((resolve, reject) => {
link.onload = () => {
console.log(`Font preloaded successfully: ${fontUrl}`);
resolve();
};
link.onerror = () => {
console.error(`Font preload failed: ${fontUrl}`);
reject(new Error(`Failed to preload: ${fontUrl}`));
};
document.head.appendChild(link);
});
}
/**
* Preload multiple fonts in sequence
*/
async function preloadFontsSequentially() {
const fonts = [
{ url: '/fonts/critical.woff2', importance: 'high' },
{ url: '/fonts/primary.woff2', importance: 'auto' },
{ url: '/fonts/secondary.woff2', importance: 'low' }
];
for (const font of fonts) {
try {
await preloadFontWithFallback(font.url);
} catch (error) {
console.error(error);
// Continue with next font even if one fails
}
}
}
/**
* Monitor font loading performance
*/
function monitorFontPerformance() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'link' && entry.name.includes('.woff')) {
console.log('Font loading metrics:', {
name: entry.name,
duration: entry.duration,
transferSize: entry.transferSize,
startTime: entry.startTime
});
}
}
});
observer.observe({ entryTypes: ['resource'] });
}
}
// Initialize
smartFontPreload();
monitorFontPerformance();
Description: Comprehensive strategies for managing Flash of Unstyled Text, Flash of Invisible Text, and Flash of Faux Text.
/**
* ==========================================
* FOUT (Flash of Unstyled Text) Strategy
* ==========================================
*
* Problem: System font shows first, then web font replaces it
* Causes: font-display: swap
* Impact: Visual jump, potential CLS issues
*
* Solution: Use font-display: swap with matching fallback metrics
*/
/**
* Step 1: Define web font with font-display: swap
*/
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2') format('woff2');
font-weight: 400;
font-display: swap; /* Causes FOUT */
}
/**
* Step 2: Create metric-compatible fallback
*
* Use size-adjust to match fallback font metrics to web font
* This minimizes CLS when fonts swap
*/
@font-face {
font-family: 'Custom Font Fallback';
src: local('Arial');
size-adjust: 97.5%; /* Adjust to match web font */
ascent-override: 95%;
descent-override: 20%;
line-gap-override: 0%;
}
/**
* Step 3: Use fallback in font stack
*/
body {
font-family: 'Custom Font', 'Custom Font Fallback', Arial, sans-serif;
}
/**
* Alternative: Use CSS class to control FOUT
*/
.fonts-loading body {
font-family: Arial, sans-serif;
}
.fonts-loaded body {
font-family: 'Custom Font', Arial, sans-serif;
}
/**
* ==========================================
* FOIT (Flash of Invisible Text) Strategy
* ==========================================
*
* Problem: Text is invisible until web font loads
* Causes: font-display: block (or old browser default)
* Impact: Poor UX, delayed LCP, users might miss content
*
* Solution: Use font-display: swap or fallback with timeout
*/
/**
* Prevent FOIT with font-display: swap
*/
@font-face {
font-family: 'No FOIT Font';
src: url('/fonts/font.woff2') format('woff2');
font-display: swap; /* Prevents FOIT */
}
/**
* For critical brand fonts where FOIT is acceptable,
* minimize block period
*/
@font-face {
font-family: 'Brand Font';
src: url('/fonts/brand.woff2') format('woff2');
font-display: fallback; /* Very short block period (~100ms) */
}
/**
* ==========================================
* FOFT (Flash of Faux Text) Strategy
* ==========================================
*
* Problem: Browser synthesizes bold/italic until real variants load
* Causes: Missing font weight/style variants
* Impact: Faux bold/italic looks different from real variants
*
* Solution: Two-stage font loading or font-synthesis control
*/
/**
* Step 1: Load regular weight first
*/
@font-face {
font-family: 'Progressive Font';
src: url('/fonts/font-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/**
* Step 2: Load other weights/styles
*/
@font-face {
font-family: 'Progressive Font';
src: url('/fonts/font-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Progressive Font';
src: url('/fonts/font-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/**
* Disable font synthesis to prevent faux bold/italic
*/
body {
font-family: 'Progressive Font', sans-serif;
font-synthesis: none; /* Prevents browser from faking bold/italic */
}
/**
* Alternative: Control synthesis separately
*/
.no-faux-bold {
font-synthesis-weight: none; /* Prevent faux bold */
}
.no-faux-italic {
font-synthesis-style: none; /* Prevent faux italic */
}
/**
* ==========================================
* Complete FOUT/FOIT/FOFT Solution
* ==========================================
*
* Comprehensive approach using all techniques
*/
/**
* 1. Define metric-compatible fallback
*/
@font-face {
font-family: 'Roboto Fallback';
src: local('Arial');
size-adjust: 100.5%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}
/**
* 2. Define web font with optimal font-display
*/
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
/**
* 3. Use progressive font stack
*/
body {
font-family: 'Roboto', 'Roboto Fallback', Arial, sans-serif;
font-synthesis: none;
}
/**
* 4. Style loading states
*/
.fonts-loading {
/* Styles during font loading */
}
.fonts-loaded {
/* Styles after fonts load */
}
.fonts-failed {
/* Styles if fonts fail to load */
}
/**
* ==========================================
* Critical CSS Approach
* ==========================================
*
* Inline critical font CSS to prevent FOUT on first load
*/
<!-- Critical font CSS inlined in <head> -->
<style>
/**
* Inline only the most critical @font-face declaration
* This prevents FOUT for above-the-fold content
*/
@font-face {
font-family: 'Critical Font';
src: url('/fonts/critical.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
/* Critical styles using the font */
h1, h2, .hero {
font-family: 'Critical Font', Arial, sans-serif;
}
</style>
<!-- Non-critical font CSS loaded asynchronously -->
<link rel="preload"
href="/css/non-critical-fonts.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'">
/**
* JavaScript-based FOUT/FOIT/FOFT Management
*/
/**
* Font Face Observer - Detect font loading and manage classes
*
* Install: npm install fontfaceobserver
*/
import FontFaceObserver from 'fontfaceobserver';
/**
* Two-stage font loading (FOFT pattern)
*
* Stage 1: Load regular weight immediately
* Stage 2: Load other weights after regular is ready
*
* Benefits:
* - Faster initial render with regular weight
* - Prevents FOIT for all text
* - Progressive enhancement
*/
class FontLoadingManager {
constructor() {
this.html = document.documentElement;
this.sessionKey = 'fonts-loaded';
// Check if fonts were loaded in previous session
if (sessionStorage.getItem(this.sessionKey)) {
this.html.classList.add('fonts-loaded');
return;
}
this.loadFonts();
}
/**
* Stage 1: Load critical fonts
*/
async loadCriticalFonts() {
this.html.classList.add('fonts-loading');
const regular = new FontFaceObserver('Roboto', { weight: 400 });
try {
await regular.load(null, 5000); // 5 second timeout
this.html.classList.add('fonts-stage-1');
console.log('Stage 1 fonts loaded');
return true;
} catch (error) {
console.error('Stage 1 fonts failed to load:', error);
this.html.classList.add('fonts-failed');
return false;
}
}
/**
* Stage 2: Load additional fonts
*/
async loadAdditionalFonts() {
const bold = new FontFaceObserver('Roboto', { weight: 700 });
const italic = new FontFaceObserver('Roboto', { style: 'italic' });
try {
await Promise.all([
bold.load(null, 5000),
italic.load(null, 5000)
]);
this.html.classList.remove('fonts-loading', 'fonts-stage-1');
this.html.classList.add('fonts-loaded');
// Store in session to prevent FOUT on subsequent page loads
sessionStorage.setItem(this.sessionKey, 'true');
console.log('All fonts loaded');
} catch (error) {
console.error('Stage 2 fonts failed to load:', error);
// Stage 1 fonts still work
this.html.classList.remove('fonts-loading');
this.html.classList.add('fonts-partial');
}
}
/**
* Orchestrate two-stage loading
*/
async loadFonts() {
const stage1Success = await this.loadCriticalFonts();
if (stage1Success) {
// Load stage 2 fonts without blocking
requestIdleCallback(() => {
this.loadAdditionalFonts();
});
}
}
}
// Initialize font loading manager
new FontLoadingManager();
/**
* CSS Font Loading API - Native browser API
*/
class NativeFontLoader {
constructor() {
this.html = document.documentElement;
}
/**
* Load fonts using native Font Loading API
*/
async loadFonts() {
if (!('fonts' in document)) {
console.warn('Font Loading API not supported');
return;
}
this.html.classList.add('fonts-loading');
try {
// Wait for all fonts to be ready
await document.fonts.ready;
this.html.classList.remove('fonts-loading');
this.html.classList.add('fonts-loaded');
console.log('All fonts ready');
// Check which fonts loaded
document.fonts.forEach(font => {
console.log(`${font.family} ${font.weight} ${font.style}: ${font.status}`);
});
} catch (error) {
console.error('Font loading error:', error);
this.html.classList.add('fonts-failed');
}
}
/**
* Load specific font programmatically
*/
async loadSpecificFont(family, url, options = {}) {
const {
weight = '400',
style = 'normal',
display = 'swap'
} = options;
const font = new FontFace(family, `url(${url})`, {
weight,
style,
display
});
try {
const loadedFont = await font.load();
document.fonts.add(loadedFont);
console.log(`Loaded: ${family}`);
return true;
} catch (error) {
console.error(`Failed to load ${family}:`, error);
return false;
}
}
/**
* Monitor font loading progress
*/
monitorFontLoading() {
document.fonts.addEventListener('loadingdone', (event) => {
console.log('Font loading done:', event.fontfaces.length, 'fonts');
});
document.fonts.addEventListener('loadingerror', (event) => {
console.error('Font loading error:', event);
});
}
}
/**
* Prevent layout shift during font swap
*/
class LayoutShiftPrevention {
/**
* Calculate and apply font metrics to reduce CLS
*/
static calculateFallbackMetrics(webFont, fallbackFont) {
// This is a simplified example
// Use tools like fontaine or next/font for accurate calculations
const webFontMetrics = {
ascent: 1000,
descent: 250,
lineGap: 0,
unitsPerEm: 1000
};
const fallbackMetrics = {
ascent: 905,
descent: 212,
lineGap: 0,
unitsPerEm: 1000
};
const sizeAdjust = webFontMetrics.unitsPerEm / fallbackMetrics.unitsPerEm;
const ascentOverride = (webFontMetrics.ascent / webFontMetrics.unitsPerEm) * 100;
const descentOverride = (webFontMetrics.descent / webFontMetrics.unitsPerEm) * 100;
return {
sizeAdjust: `${sizeAdjust * 100}%`,
ascentOverride: `${ascentOverride}%`,
descentOverride: `${descentOverride}%`,
lineGapOverride: '0%'
};
}
}
// Initialize
const fontLoader = new NativeFontLoader();
fontLoader.loadFonts();
fontLoader.monitorFontLoading();
Description: Advanced techniques for optimizing font delivery and performance, including Core Web Vitals impact.
/**
* ==========================================
* Font Performance Audit Tool
* ==========================================
*
* Comprehensive tool for analyzing font loading performance
* and its impact on Core Web Vitals
*/
class FontPerformanceAuditor {
constructor() {
this.metrics = {
fonts: [],
totalSize: 0,
totalDuration: 0,
clsImpact: 0,
lcpImpact: 0
};
}
/**
* Collect font loading metrics using Performance API
*/
collectFontMetrics() {
if (!window.PerformanceObserver) {
console.warn('PerformanceObserver not supported');
return;
}
// Observe resource timing for fonts
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (this.isFontResource(entry)) {
this.metrics.fonts.push({
name: this.getFontName(entry.name),
url: entry.name,
size: entry.transferSize,
duration: entry.duration,
startTime: entry.startTime,
renderBlockingStatus: entry.renderBlockingStatus,
protocol: entry.nextHopProtocol
});
this.metrics.totalSize += entry.transferSize;
this.metrics.totalDuration += entry.duration;
}
}
});
observer.observe({ entryTypes: ['resource'] });
}
/**
* Check if resource is a font
*/
isFontResource(entry) {
return entry.name.match(/\.(woff2?|ttf|otf|eot)(\?.*)?$/) ||
entry.initiatorType === 'link' && entry.name.includes('fonts');
}
/**
* Extract font name from URL
*/
getFontName(url) {
const match = url.match(/([^\/]+)\.(woff2?|ttf|otf|eot)/);
return match ? match[1] : 'unknown';
}
/**
* Measure Cumulative Layout Shift (CLS) impact
*/
measureCLSImpact() {
if (!window.PerformanceObserver) return;
let clsScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.metrics.clsImpact = clsScore;
});
observer.observe({ type: 'layout-shift', buffered: true });
}
/**
* Measure Largest Contentful Paint (LCP) impact
*/
measureLCPImpact() {
if (!window.PerformanceObserver) return;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcpImpact = lastEntry.renderTime || lastEntry.loadTime;
// Check if LCP element uses web fonts
if (lastEntry.element) {
const computedStyle = window.getComputedStyle(lastEntry.element);
const fontFamily = computedStyle.fontFamily;
console.log('LCP element font:', fontFamily);
}
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
/**
* Generate performance report
*/
generateReport() {
// Wait for fonts to load
setTimeout(() => {
const report = {
summary: {
totalFonts: this.metrics.fonts.length,
totalSize: this.formatBytes(this.metrics.totalSize),
totalDuration: `${this.metrics.totalDuration.toFixed(2)}ms`,
clsScore: this.metrics.clsImpact.toFixed(4),
lcpTime: `${this.metrics.lcpImpact.toFixed(2)}ms`
},
fonts: this.metrics.fonts,
recommendations: this.generateRecommendations()
};
console.table(this.metrics.fonts);
console.log('Performance Summary:', report.summary);
console.log('Recommendations:', report.recommendations);
return report;
}, 5000); // Wait 5 seconds for fonts to load
}
/**
* Generate performance recommendations
*/
generateRecommendations() {
const recommendations = [];
// Check total font size
if (this.metrics.totalSize > 300000) { // 300KB
recommendations.push({
severity: 'high',
message: `Total font size is ${this.formatBytes(this.metrics.totalSize)}. Consider subsetting or using fewer fonts.`,
action: 'Reduce font file sizes through subsetting or variable fonts'
});
}
// Check number of fonts
if (this.metrics.fonts.length > 5) {
recommendations.push({
severity: 'medium',
message: `Loading ${this.metrics.fonts.length} font files. Each font requires a separate request.`,
action: 'Consolidate fonts or use variable fonts'
});
}
// Check for non-WOFF2 fonts
const nonWOFF2 = this.metrics.fonts.filter(f => !f.url.includes('.woff2'));
if (nonWOFF2.length > 0) {
recommendations.push({
severity: 'medium',
message: `${nonWOFF2.length} font(s) not using WOFF2 format`,
action: 'Convert fonts to WOFF2 for better compression'
});
}
// Check CLS impact
if (this.metrics.clsImpact > 0.1) {
recommendations.push({
severity: 'high',
message: `CLS score of ${this.metrics.clsImpact.toFixed(4)} exceeds recommended threshold (0.1)`,
action: 'Use font-display: optional or implement metric-compatible fallbacks'
});
}
// Check for HTTP/1.1
const http1Fonts = this.metrics.fonts.filter(f => f.protocol === 'http/1.1');
if (http1Fonts.length > 0) {
recommendations.push({
severity: 'low',
message: `${http1Fonts.length} font(s) loaded over HTTP/1.1`,
action: 'Enable HTTP/2 for multiplexing and better performance'
});
}
return recommendations;
}
/**
* Format bytes to human-readable string
*/
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
}
// Initialize auditor
const auditor = new FontPerformanceAuditor();
auditor.collectFontMetrics();
auditor.measureCLSImpact();
auditor.measureLCPImpact();
// Generate report after page load
window.addEventListener('load', () => {
auditor.generateReport();
});
/**
* ==========================================
* Service Worker Font Caching Strategy
* ==========================================
*
* Aggressive caching for fonts to improve repeat visit performance
*/
// service-worker.js
const FONT_CACHE = 'font-cache-v1';
const FONT_URLS = [
'/fonts/roboto-regular.woff2',
'/fonts/roboto-bold.woff2',
'/fonts/inter-var.woff2'
];
/**
* Install event - precache critical fonts
*/
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(FONT_CACHE)
.then((cache) => {
console.log('Precaching fonts');
return cache.addAll(FONT_URLS);
})
);
});
/**
* Fetch event - serve fonts from cache with long expiry
*/
self.addEventListener('fetch', (event) => {
// Only handle font requests
if (!event.request.url.match(/\.(woff2?|ttf|otf|eot)$/)) {
return;
}
event.respondWith(
caches.match(event.request)
.then((cachedResponse) => {
if (cachedResponse) {
console.log('Serving font from cache:', event.request.url);
return cachedResponse;
}
// Not in cache, fetch and cache
return fetch(event.request)
.then((response) => {
// Only cache successful responses
if (!response || response.status !== 200) {
return response;
}
const responseToCache = response.clone();
caches.open(FONT_CACHE)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
/**
* ==========================================
* HTTP/2 Server Push for Fonts
* ==========================================
*
* Server configuration examples for font pushing
*/
// Express.js with http2
/**
* Server-side font pushing with HTTP/2
*
* Requires:
* - Node.js http2 module
* - SSL certificate (HTTP/2 requires HTTPS)
*/
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const server = http2.createSecureServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
});
server.on('stream', (stream, headers) => {
const reqPath = headers[':path'];
// Push critical fonts when HTML is requested
if (reqPath === '/' || reqPath === '/index.html') {
// Push critical fonts
pushFont(stream, '/fonts/roboto-regular.woff2');
pushFont(stream, '/fonts/roboto-bold.woff2');
// Serve HTML
stream.respond({
':status': 200,
'content-type': 'text/html'
});
stream.end(fs.readFileSync('index.html'));
}
});
/**
* Push font helper function
*/
function pushFont(stream, fontPath) {
const fullPath = path.join(__dirname, 'public', fontPath);
stream.pushStream({ ':path': fontPath }, (err, pushStream) => {
if (err) {
console.error('Push error:', err);
return;
}
pushStream.respond({
':status': 200,
'content-type': 'font/woff2',
'cache-control': 'public, max-age=31536000, immutable'
});
const fontFile = fs.createReadStream(fullPath);
fontFile.pipe(pushStream);
});
}
server.listen(3000);
// Nginx configuration for HTTP/2 push
/**
* Nginx configuration example
*
* Add to nginx.conf or site configuration
*/
/*
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
root /var/www/html;
location / {
# Push critical fonts
http2_push /fonts/roboto-regular.woff2;
http2_push /fonts/roboto-bold.woff2;
try_files $uri $uri/ /index.html;
}
location ~* \.(woff2?|ttf|otf|eot)$ {
# Font caching headers
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
}
}
*/
<!--
==========================================
HTML Meta Tags for Font Performance
==========================================
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--
Resource hints for font optimization
-->
<!-- Preconnect to font CDN -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preload critical fonts -->
<link rel="preload"
href="/fonts/roboto-regular.woff2"
as="font"
type="font/woff2"
crossorigin>
<!-- DNS prefetch for fallback CDN -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<!--
Feature policy for font-display
Enforce font-display policy across all fonts
-->
<meta http-equiv="Feature-Policy"
content="font-display 'swap'">
<title>Font Performance Optimization</title>
</head>
<body>
<h1>Optimized Font Loading</h1>
</body>
</html>
When implementing web fonts, always:
Format Selection
Loading Strategy
font-display: swap for most fontsfont-display: optional for non-critical fontsPerformance
Core Web Vitals
Subsetting
Self-Hosting
Variable Fonts
Testing
crossorigin attributeAlways prioritize user experience and performance when implementing web fonts. A well-optimized font loading strategy can significantly improve perceived performance and Core Web Vitals scores.
Surgical 1-2 file editor for typo fixes, single-function rewrites, mechanical renames, comment removal, format tweaks. Refuses 3+ files, new features, cross-file changes. Returns caveman diff receipt.
Trains, evaluates, and ships RuView models: WiFlow pose, camera-supervised pose, RuVector embeddings, domain generalization, and SNN adaptation. Handles GPU training on GCloud and Hugging Face publishing.
npx claudepluginhub ehssanatassi/ui-marketplace --plugin web-fonts