Sacred Lantern Studio
2026-11-08· 6 minPerformanceWebPImagesLighthouse

Four screenshots: 955 KB down to 105 KB with WebP

The same UI screenshots were 955 KB as PNG and 105 KB as WebP. The 6x compression isn't from lowering quality — screenshots dominated by flat colour compress to a fraction in WebP. Parameters and measurements included.

Lighthouse flagged it: Improve image delivery — Est savings of 537 KiB.

Four screenshots in the case-study section of my homepage. All 1200×777 PNG. 955 KB combined, two thirds of the page's total transfer.

After converting to WebP: 105 KB. An 850 KB saving.

TL;DR

File PNG WebP Ratio
shot-tool.png 283 KB 17 KB 6%
shot-shop.png 195 KB 34 KB 17%
shot-shop-en.png 184 KB 33 KB 18%
shot-tool-en.png 291 KB 20 KB 7%
Total 955 KB 105 KB 11%

Best case 6%, worst case 18%. The spread comes from image content, explained below.


Why 6% is possible

This isn't brute-forced lossy compression. Look at what's actually in these images:

  • Dark background (#08090b) covering 70%+
  • Text in white and a handful of greys
  • No photographs, no gradient noise

Large flat areas plus a small palette is exactly what WebP is built for.

PNG compression (DEFLATE) is row-by-row lossless. It's inefficient against "huge regions of identical pixels" — every pixel still carries an index. WebP stores "this whole block is this colour" directly.

For contrast: photographs, complex gradients, and lots of anti-aliased edges land at 40–60% compression. 6% is a flat-colour special case, not a general rule.


The parameters

from PIL import Image
import os

for name in ["shot-shop.png", "shot-tool.png", "shot-shop-en.png", "shot-tool-en.png"]:
    im = Image.open(name).convert("RGB")
    out = name.replace(".png", ".webp")
    im.save(out, "WEBP", quality=82, method=6)

    o, n = os.path.getsize(name), os.path.getsize(out)
    print(f"{name:22s} {o//1024:>4} KB -> {n//1024:>4} KB  ({n*100//o}%)")

Two settings matter:

quality=82 — not arbitrary. UI screenshots show no visible loss between 80 and 85 (slight softening on text edges). Below 75 text starts to blur. Above 90 the file grows with no perceivable gain.

method=6 — trades encoding time for size. Range is 0–6; 6 is slowest and tightest. For four images that's under a second. For hundreds, use method=4.

Verify the compression didn't damage the image

Smaller file doesn't mean correct output:

import numpy as np
from PIL import Image

a = np.array(Image.open("shot-tool.png").convert("RGB")).astype(int)
b = np.array(Image.open("shot-tool.webp").convert("RGB")).astype(int)
diff = np.abs(a - b)
print("max channel delta:", diff.max())
print("mean delta:", diff.mean())

Output: max 14, mean 2.14 (out of 255). The max occurs on anti-aliased text edges — invisible.

Don't skip this step. I once compressed a hairline-diagram image to 20% of its size and the lines developed ringing artifacts. Size alone would have said "success".


Update the references, not just the files

After generating WebP, the src attributes must change:

sed -i 's|/images/shot-tool\.png|/images/shot-tool.webp|g' app/**/page.tsx

If you forget, the browser keeps requesting the old PNG and you've compressed nothing.

I did forget once: files converted, src untouched, re-ran Lighthouse, score didn't move. Half an hour wasted.


The Next.js Image trap

If you use <Image>, watch unoptimized:

<Image src="/images/shot-tool.webp" width={1200} height={777} unoptimized />

With unoptimized, Next.js emits the file as-is — no format conversion, no multi-size variants, no hashing.

Add it when:

  • Images are already optimised before build (this case)
  • Static export (output: 'export') without an image service

Don't add it when:

  • You want automatic DPR-based variants
  • Source images come from user uploads with unknown dimensions

I kept unoptimized here: WebP is already at the limit, Next re-compressing adds little and another cache key.


Icons in the same pass

File Before After Method
favicon.ico 20.6 KB 2.8 KB 16/32 only
apple-touch-icon.png 39 KB 14 KB 180×180, 128 colours
logo-192.png 44 KB 15.8 KB same

The 20 KB favicon.ico held 16/32/48/64. Browsers mainly use 16 and 32; dropping the bigger sizes cut it to 2.8 KB:

src = Image.open("logo-512.png").convert("RGBA")
src.save("favicon.ico", format="ICO", sizes=[(16,16), (32,32)])

Note the .convert("RGBA") — saving an RGB image as ICO makes Next.js fail the build with The PNG is not in RGBA format!.


End result

Metric Before After
Four screenshots 955 KB 105 KB
Page transfer ~1.5 MB 424 KiB
Lighthouse image audit 537 KiB savings flagged passes
Performance score 93 97

No quality loss, no layout change. Just a different container.


When not to do this

Type Advice
UI screenshots, charts, flat illustration WebP wins big (this case)
Photos, complex gradients 40–60%, still worth it
Needs alpha + lossless PNG or WebP lossless — don't set quality
Icons, simple geometry SVG, an order of magnitude smaller
Social/OG images Keep JPG/PNG — WebP support in link unfurlers is still patchy

That last one bites: an og:image in WebP renders as a blank card on some platforms.


One line

Look at what the image is made of before picking a format. Flat-colour screenshots go below 10% in WebP; photographs stop around half — both beat shipping the PNG.

Does your site have these problems?

Run the free audit tool for a 30-second report, or just write to me and describe the situation — I will tell you whether it is worth fixing.