Fix Custom Font & Image Bloat in 15 Minutes

Cut font and image payload from 2.8MB to 340KB using AI compression and font subsetting—boost mobile performance by 66%.

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:

  • preload fetches font during HTML parse (not after CSS loads)
  • font-display: swap prevents invisible text
  • unicode-range tells 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:

MetricBeforeAfterImprovement
FCP3.2s1.1s66% faster
LCP4.8s1.6s67% faster
Total Size2.8MB340KB88% 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:

Tested on Chrome 131, Safari 18, Firefox 133 | Node.js 22.x | February 2026