treeru.com

Image Optimization Alone Boosted PageSpeed by 20 Points — 36MB to 0.8MB

What's the single most impactful thing you can do for web performance? Code splitting? Caching? Neither. Image compression. It's the easiest, highest-impact, lowest-risk optimization available. We compressed 6 images from 36MB to 0.8MB(97.8% reduction) using sharp, optimized loading strategies, and gained 28 points on Desktop PageSpeed. LCP dropped from 110 seconds to 6.8 seconds.

The Problem — 6 Images, 36MB Total

The site used 6 images total. They'd been converted to WebP from high-resolution originals during design, but at the original resolution with default compression settings.

ImageUsageFile SizeResolution
Hero MainFull-screen hero5.6MB1856x2304
Service (AI)Service card5.3MB1920x1434
Service (Network)Service card6.6MB1920x1434
Service (Software)Service card5.9MB1920x1434
Background TextureSection background6.0MB1920x1072
Background PhotoCTA background6.1MB1920x1072
Total35.5MB

Google PageSpeed Insights simulates Mobile as Moto G Power + slow 4G (1.6 Mbps). Downloading 36MB at that speed takes approximately 180 seconds. An LCP measurement of 110 seconds was the inevitable result.

Here's the critical insight: a 5MB WebP and a 100KB WebP look identical on most monitors. At web resolutions (1920px and below), quality 80 and quality 100 are visually indistinguishable. Bigger file size does not mean better quality for the user.

Compression with sharp — The Fastest Node.js Image Library

sharp is the fastest image processing library for Node.js, built on libvips. It's faster than Photoshop or online tools and fully scriptable for automation.

npm install sharp --save-dev

// Single image compression
const sharp = require('sharp');

// WebP — general images
await sharp('input.webp')
  .resize(1920)            // Width-based resize
  .webp({ quality: 80 })   // Quality 80 is sufficient
  .toFile('output.webp');

// AVIF — Hero image (aggressive compression)
await sharp('hero.webp')
  .resize(900)             // Mobile-first width
  .avif({ quality: 40, effort: 6 })
  .toFile('hero.avif');
UsageFormatMax WidthQualityNotes
Hero (LCP image)AVIF~900px35–50Most aggressive compression
Service cardsWebP~600px40–50Apply lazy loading
Background textureWebP~1200px30–40Detail less critical
Background photoWebP~1200px40–50Apply lazy loading

Compression Results

ImageBeforeAfterReduction
Hero Main5,695KB124KB97.8%
Service (AI)5,406KB93KB98.3%
Service (Network)6,789KB214KB96.8%
Service (Software)6,082KB155KB97.5%
Background Texture6,170KB122KB98.0%
Background Photo6,282KB97KB98.5%
Total35.5MB0.8MB97.8%

WebP vs AVIF — When to Use Which

Both formats are supported by modern browsers, but the choice depends on the use case.

AttributeWebPAVIF
Compression ratioGoodExcellent (40–60% smaller)
Encoding speedFastSlow (~6x slower)
Decoding speedFastSlightly slower
Browser support96%+92%+
Quality (same size)GoodBetter

Use AVIF for the Hero image (LCP). It's a single image, so encoding time is negligible, and the file size difference directly impacts LCP. Use WebP for everything else — it's sufficient quality with faster encoding. When processing dozens of images, AVIF encoding time becomes a real build-time concern.

Loading Strategy — fetchPriority and lazy loading

Compression alone isn't enough. You need to tell the browser which images to load first.

Above the fold (Hero image):

<img
  src="/images/hero.avif"
  alt="description"
  width={900}
  height={1118}
  fetchPriority="high"
/>

Use fetchPriority="high" for highest-priority loading. Always specify width and height to reserve layout space. Never add loading="lazy" to the Hero image.

Below the fold (cards, backgrounds):

<img
  src="/images/card.webp"
  alt="description"
  width={600}
  height={448}
  loading="lazy"
/>

Use loading="lazy" so these load only when entering the viewport. This prevents bandwidth competition with the Hero image and saves initial load data.

Why width and height matter: Explicit dimensions let the browser reserve exact space before the image loads, preventing CLS (Cumulative Layout Shift). Even if CSS controls the rendered size, HTML attributes provide the intrinsic aspect ratio the browser uses for layout calculation.

The Preload Trap — Less Is More

"Important images should be preloaded for faster loading" — correct, but only for the single LCP image.

<!-- Preload only the LCP image in layout.tsx <head> -->
<link
  rel="preload"
  href="/images/hero.avif"
  as="image"
  type="image/avif"
/>

Next.js auto-preload warning: Next.js automatically generates preload tags for img elements without loading="lazy". If you forget lazy on below-fold images, unintended preloads are added — these compete with the Hero image for bandwidth. The result: LCP actually gets slower. In our case, forgetting lazy on 3 below-fold images created 4 preloads (Hero + 3 extras). Adding lazy and keeping only the Hero preload improved LCP significantly.

Results

MetricBeforeAfter
Mobile Performance3845
Desktop Performance5280
LCP (Mobile)110.2s6.8s
Speed Index (Mobile)46.4s6.3s
CLS0.5000.481

Why only +7 on Mobile? Despite reducing images by 97%, Mobile only gained 7 points because CLS remained at 0.481 — still in the "Poor" range. Lighthouse allocates 25% of the total score to CLS. After fixing CLS separately (in the next optimization phase), Mobile jumped from 45 to 70 (+25 points). Image optimization enables the improvement, but CLS must also be addressed for the full score gain.

Summary

Image optimization is step one — easiest to implement, highest impact, and lowest risk of any performance optimization.

sharp with WebP quality 80 and max width 1920px achieves 97%+ reduction. No visible quality loss at web display resolutions.

Hero image uses AVIF + fetchPriority="high". Everything else uses WebP + loading="lazy". This separation ensures the LCP image loads first without bandwidth competition.

Preload only the LCP image — adding preload to multiple images creates bandwidth contention that slows down the most important one.

Always specify width and height on img tags to prevent CLS, regardless of CSS sizing. The browser uses these for aspect ratio calculation before the image loads.