Modern websites load 2-4MB of fonts and images by default. Here's how to cut that to under 300KB using AI-powered compression—without sacrificing quality.
You'll learn:
- Automated font subsetting for 90% size reduction
- AI image compression that beats manual optimization
- Cache strategies that work in 2026
Time: 15 min | Level: Intermediate
Problem: Fonts and Images Kill Performance
Your Lighthouse score is 45 because custom fonts block rendering for 2.3 seconds and hero images take 4 seconds to load on 4G. Users bounce before seeing content.
Common symptoms:
- First Contentful Paint (FCP) > 3 seconds
- Cumulative Layout Shift from font swapping
- Mobile users see blank screens
- Perfect desktop performance, terrible mobile
Why This Happens
Most developers load entire font families (regular, bold, italic = 600KB+) when they use 3 styles. Images get exported at 100% quality because "it looks fine" without testing compression ratios.
The math:
- Full Google Font family: ~400KB
- Unoptimized PNG hero image: 2.1MB
- Total: 2.5MB before any JavaScript loads
Solution
Step 1: Subset Your Fonts with Glyphhanger
# Install glyphhanger (uses Puppeteer to detect used characters)
npm install -g glyphhanger
# Subset a local font file to only characters you use
glyphhanger --subset=Inter-Regular.ttf --formats=woff2 \
--US_ASCII --whitelist="€£¥"
Why this works: If you only use A-Z, a-z, 0-9, and basic punctuation, you need ~200 glyphs instead of 2,000+. This reduces file size by 85-90%.
Expected output:
Subsetting Inter-Regular.ttf
Original: 385KB
Subset: 42KB (89% reduction)
Glyphs: 2,048 → 214
Step 2: Self-Host with Optimal Loading
<!-- In <head>, before other CSS -->
<link rel="preload" href="/fonts/Inter-subset.woff2" as="font"
type="font/woff2" crossorigin>
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-subset.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
unicode-range: U+0020-007F; /* ASCII only */
}
body {
font-family: Inter, system-ui, -apple-system, sans-serif;
}
</style>
Critical details:
preloadfetches font during HTML parse (not after CSS loads)font-display: swapprevents invisible textunicode-rangetells browser exactly what's covered
If it fails:
- CORS error: Add
Access-Control-Allow-Origin: *to font file headers - Still seeing fallback flash: Font might not be subsetting correctly—check file size
Step 3: AI Image Compression
Use Squoosh CLI (Google's AI compressor) for automated batch processing:
# Install Squoosh
npm install -g @squoosh/cli
# Compress all images to WebP with AI optimization
squoosh-cli --webp auto images/*.{jpg,png} -d compressed/
What "auto" does: Uses machine learning to find the lowest file size where SSIM (structural similarity) stays above 0.95—visually identical to humans but 60-80% smaller.
Example results:
hero.jpg (2.1MB) → hero.webp (287KB) - 86% reduction
profile.png (456KB) → profile.webp (34KB) - 93% reduction
Step 4: Implement Responsive Images
<picture>
<!-- Serve smaller images to mobile -->
<source media="(max-width: 768px)"
srcset="/img/hero-400w.webp 400w, /img/hero-800w.webp 800w"
type="image/webp">
<!-- Desktop gets larger version -->
<source srcset="/img/hero-1200w.webp 1200w, /img/hero-1600w.webp 1600w"
type="image/webp">
<!-- Fallback for old browsers -->
<img src="/img/hero-800w.jpg"
alt="Dashboard showing real-time analytics"
loading="lazy"
decoding="async">
</picture>
Why multiple sizes: Mobile devices with 375px screens don't need 1920px images. Browser downloads only the appropriate size.
Step 5: Set Up Build Automation
// vite.config.js or next.config.js
import { squooshPlugin } from 'vite-plugin-squoosh';
export default {
plugins: [
squooshPlugin({
// Compress images during build
webp: { quality: 85 },
avif: { quality: 80 }, // Even better compression
}),
],
};
Or GitHub Action for CI/CD:
# .github/workflows/optimize.yml
name: Optimize Assets
on: [push]
jobs:
compress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Compress images
run: |
npm install -g @squoosh/cli
squoosh-cli --webp auto public/images/*.{jpg,png}
- name: Commit optimized
run: |
git config user.name "Asset Bot"
git add public/images/
git commit -m "Optimize images [skip ci]"
git push
Verification
Test performance:
# Install Lighthouse CI
npm install -g @lhci/cli
# Run audit
lhci autorun --collect.url=http://localhost:3000
You should see:
- Performance score: 90+
- FCP: < 1.5 seconds
- Total page weight: < 500KB
Before/After:
| Metric | Before | After | Improvement |
|---|---|---|---|
| FCP | 3.2s | 1.1s | 66% faster |
| LCP | 4.8s | 1.6s | 67% faster |
| Total Size | 2.8MB | 340KB | 88% smaller |
What You Learned
- Font subsetting cuts 85-90% without visible changes
- AI compression (Squoosh) beats manual Photoshop exports
- Self-hosting fonts is faster than Google Fonts CDN in 2026
- Responsive images prevent mobile users downloading desktop assets
When NOT to use this:
- Static sites with <10 images (manual optimization is fine)
- If you need full Unicode support (subsetting won't work for multilingual sites)
- Brand guidelines require exact font rendering (test subsetting carefully)
Tools used:
- Glyphhanger - Font subsetting
- Squoosh CLI - AI image compression
- Lighthouse CI - Performance testing
Tested on Chrome 131, Safari 18, Firefox 133 | Node.js 22.x | February 2026