Image Optimization Techniques for Faster Page Loads
Your hero image is 4.2 MB. Your LCP is 6.8 seconds. Google is burying you on page four, and every second of delay costs you roughly 7% in conversions. Here's how to fix it without rewriting your app. This guide walks through the image optimization techniques that actually move the needle on Core Web Vitals: picking the right format, compressing without wrecking quality, serving responsive sizes, lazy-loading correctly, and automating the whole pipeline in CI.
Why Images Are Usually the Bottleneck
A quick reality check before we touch any code. According to HTTP Archive's Web Almanac, images account for roughly half the total bytes of an average page load. JavaScript gets all the attention, but images are usually the heavier payload. The fix isn't one trick. It's a stack of techniques that compound:
- Format choice - the biggest single win, often 50-80% size reduction.
- Compression - trims the fat without visible quality loss.
- Responsive delivery - stop sending a 2400px image to a 375px phone.
- Lazy loading - defer offscreen images so they don't block the initial paint.
- Automation - make it repeatable so it survives the next sprint.
Let's go through each.
Step 1: Choose the Right Format
Image format is the file type that determines how pixels are stored and compressed. Three formats matter in 2024:
- WebP - Google's format, supported by every modern browser. Typically 25-35% smaller than JPEG at equivalent quality. Safe default.
- AVIF - the newer format, based on the AV1 codec. Often 50% smaller than JPEG, with better quality at low bitrates. Browser support is now solid (Chrome, Firefox, Safari 16.4+). The catch: encoding is slow, so it's best done at build time.
- JPEG/PNG - keep JPEG for legacy fallback, keep PNG only when you genuinely need lossless transparency (logos, screenshots of text).
The practical move: generate AVIF and WebP, fall back to JPEG. Here's how with ``: The browser picks the first format it understands. Old browsers get the JPEG. Everyone else gets the smaller file. Notice the width and height attributes. Always set them. They tell the browser how much space to reserve, which prevents layout shift - the annoying jump when an image finally loads. Layout shift is measured by CLS (Cumulative Layout Shift), one of the three Core Web Vitals.
Step 2: Compress Without Visible Quality Loss
Once you've picked a format, tune the compression. For most photos, a quality setting of 75-82 in WebP or AVIF is visually indistinguishable from the original - but the file is dramatically smaller. If you're on the command line, sharp is the workhorse. Here's a Node script that converts a folder of images to both formats:
import sharp from 'sharp';
import { readdir } from 'fs/promises';
import path from 'path';
const INPUT_DIR = './src/images';
const OUTPUT_DIR = './public/images';
const files = await readdir(INPUT_DIR);
for (const file of files) {
const input = path.join(INPUT_DIR, file);
const name = path.parse(file).name;
await sharp(input)
.resize({ width: 1600, withoutEnlargement: true })
.avif({ quality: 65 })
.toFile(path.join(OUTPUT_DIR, `${name}.avif`));
await sharp(input)
.resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(path.join(OUTPUT_DIR, `${name}.webp`));
}
console.log(`Processed ${files.length} images.`);
Run it and compare a few outputs side by side. You'll usually find quality 65 AVIF looks identical to the original JPEG at a third of the size.
Step 3: Serve Responsive Sizes with srcset
A 1600px image on a 375px phone wastes bandwidth and CPU. The phone has to decode and downscale pixels it will never display. Use srcset and sizes to let the browser pick the right file. The w descriptor tells the browser each file's intrinsic width. The sizes attribute describes how wide the image will render at various viewports. The browser combines these with the device pixel ratio and picks the smallest sufficient file. The payoff is real. On a mobile connection, this alone can cut image bytes by 60-70%.
Step 4: Lazy-Load Correctly (and Not Too Much)
Lazy loading means deferring image requests until the image is about to enter the viewport. The native loading="lazy" attribute does this with zero JavaScript. But here's the trap: never lazy-load your LCP image. The LCP (Largest Contentful Paint) element is usually your hero image, and it's the most important thing to load fast. Lazy-loading it delays the very metric you're trying to improve. The rule:
- Hero / above-the-fold images:
loading="eager"and addfetchpriority="high". - Everything below the fold:
loading="lazy".
Also add decoding="async" to offscreen images so the browser can decode them off the main thread.
Step 5: Automate It in CI
Manual optimization rots. Someone uploads a 5 MB PNG, and six months later you're back to square one. Put the pipeline in CI so every pull request with a new image gets optimized automatically. A minimal GitHub Actions step:
- name: Optimize images
run: |
npm install sharp
node scripts/optimize-images.js
- name: Commit optimized assets
run: |
git config user.name "ci-bot"
git add public/images
git commit -m "chore: optimize images" || echo
Comments
No comments yet. Start the discussion.