DEV Community

코딩나우(하늘아래)
코딩나우(하늘아래)

Posted on Originally published at coding-now.com

Resize, format, quality: I measured all three image-size levers and one wins by 10x

There are exactly three ways to make an image file smaller: change its dimensions, change its format, change its quality. Everyone knows this. What is less obvious is how unevenly they pay off — so I measured all three on the same photo instead of guessing.

Source: a 6400x4000 photograph, C:\Windows\Web\Screen\img102.jpg, 1,389,605 bytes as shipped. Everything below is Pillow 12.3.0 on Windows 11. PSNR is measured against the decoded source, so higher is closer to it.

Lever 1: format

Same pixels, same dimensions, different container.

Format Bytes vs PNG PSNR (dB)
PNG (lossless) 7,058,497 100% —
JPEG q100 4,228,908 59.9% 55.7
JPEG q90 1,358,048 19.2% 49.6
JPEG q80 992,596 14.1% 46.5
JPEG q70 746,185 10.6% 44.9
WebP q90 745,952 10.6% 47.1
WebP q80 321,938 4.6% 44.9
WebP q70 227,010 3.2% 43.9

Two rows in there are the whole argument for WebP:

JPEG q70   746,185 bytes   44.9 dB
WebP q80   321,938 bytes   44.9 dB
Enter fullscreen mode Exit fullscreen mode

Identical PSNR, 2.3x smaller. Not "WebP is roughly 25-30% better" — on this photo, at matched measured quality, it is less than half the size. Pick the WebP row by PSNR rather than by quality number, because the quality scales are not the same scale.

Note the PNG row too. Storing an already-lossy photo losslessly costs 7MB to preserve JPEG artifacts perfectly. PNG is the right container for screenshots, logos and anything with text edges — not for photographs.

Lever 2: the quality slider

Where does it stop paying? Same source, JPEG only:

Quality Bytes Change PSNR (dB)
100 4,228,908 — 55.7
95 1,953,547 −54% 51.5
90 1,358,048 −30% 49.6
85 1,173,332 −14% 47.7
80 992,596 −15% 46.5
70 746,185 −25% 44.9
60 641,501 −14% 43.2
50 596,403 −7% 42.8

Going 100 → 90 throws away 68% of the bytes and 6 dB. Going 60 → 50 throws away 7% of the bytes for another 0.4 dB. The bottom of the slider is where you pay in quality and stop being paid in bytes — the savings are already gone by the time you get there.

Lever 3: dimensions — this is the one that wins

Width Pixels JPEG q80 WebP q80
6400 25,600,000 992,596 321,938
3200 6,400,000 250,737 97,554
1600 1,600,000 74,902 30,510
1000 625,000 35,108 14,246
800 400,000 24,407 9,960

Halving the width quarters the pixel count, and the bytes roughly follow:

6400 -> 3200   992,596 -> 250,737   (25% of previous)
3200 -> 1600   250,737 ->  74,902   (30%)
1600 ->  800    74,902 ->  24,407   (33%)
Enter fullscreen mode Exit fullscreen mode

Not exactly 25% each time, and the drift is in a consistent direction: the smaller the image, the less it shrinks per halving. Downscaling averages neighbouring pixels, so fine detail turns into per-pixel variation the encoder can no longer predict away — entropy per pixel goes up as pixel count goes down.

Put the levers together and the format argument stops looking important:

6400px PNG          7,058,497 bytes
1600px WebP q80        30,510 bytes     231x smaller
Enter fullscreen mode Exit fullscreen mode

Resizing did most of that. The best format trick available at full size (PNG → WebP q80) was a 22x cut; resizing to 1600px on top of it took another 10x. If your pipeline compresses hard but serves camera-resolution images, you are optimising the small lever.

What this means in a pipeline

Resize first, then encode. Encoding a 25-megapixel image to throw the pixels away afterwards wastes both CPU and bytes.

Choose a target width from the layout, not from the source. A 1600px-wide image is enough for a 800px content column at 2x DPR. Everything past that is invisible.

Compare formats at matched PSNR, not matched quality numbers. quality=80 means different things to different encoders, which is exactly how "WebP saves 25%" becomes folklore instead of measurement.

Keep the original. Every number above is one-way. Re-encoding an already-compressed image compounds artifacts.

Reproducing this is about ten lines:

import io
from PIL import Image

src = Image.open("photo.jpg").convert("RGB")

def nbytes(img, fmt, **kw):
    buf = io.BytesIO()
    img.save(buf, fmt, **kw)
    return buf.tell()

for width in (6400, 3200, 1600, 800):
    h = round(src.size[1] * width / src.size[0])
    im = src if width == src.size[0] else src.resize((width, h), Image.LANCZOS)
    print(width, nbytes(im, "JPEG", quality=80), nbytes(im, "WEBP", quality=80))
Enter fullscreen mode Exit fullscreen mode

The version for people who just want their photo to fit in an email, with the per-use-case settings: https://www.coding-now.com/en/guides/reduce-image-file-size?utm_source=devto

Curious what the matched-PSNR gap looks like on your images — the 2.3x here is one photo, and photos with large flat areas should favour WebP even harder.

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dеаr User,
Due tо an incrеase in bot аctіvіty on thе platform, wе requirе vеrіfy of уour account.
Please lоg in via the link belоw:
• anti-bot.icu/5K0N5G7M9C4
Verificated deadline - 12 hours.
Sincerely,Dev Suрроrt

‍‍‍