A common bug report for image tools: "I compressed my photo and the file got bigger." It sounds impossible. It isn't, and it's worth understanding, because the same mistake shows up in build pipelines and CMS plugins.
Quality is not "percent of the original"
When you save a JPEG at quality 90, the 90 doesn't mean "90% of the file you opened". It selects a set of quantization tables, which control how much detail the encoder throws away. The encoder doesn't know or care how the input was saved before.
So take a photo that was saved at quality 75, open it, and save it at quality 90. The encoder now spends more bits per block than the original did. It carefully preserves detail, including the blocky artefacts from the first save. The result looks no better than the q75 file, and it's larger.
Measure it yourself
This Node script takes any JPEG and re-encodes it at a few qualities, then saves it ten times in a row at quality 90:
// node resave.mjs photo.jpg
import { readFile } from "node:fs/promises"
import sharp from "sharp"
const original = await readFile(process.argv[2])
console.log("original".padEnd(12), (original.length / 1024).toFixed(0), "KB")
for (const quality of [95, 90, 80, 70]) {
const out = await sharp(original).jpeg({ quality }).toBuffer()
console.log(`q${quality}`.padEnd(12), (out.length / 1024).toFixed(0), "KB")
}
let gen = original
for (let i = 1; i <= 10; i++) gen = await sharp(gen).jpeg({ quality: 90 }).toBuffer()
console.log("10x q90".padEnd(12), (gen.length / 1024).toFixed(0), "KB")
Here's one run on a synthetic test image (random noise, 1600×1200, originally saved at quality 75). Real photos will give different numbers, so treat this as a demonstration and run it on yours:
original 613 KB
q95 918 KB
q90 770 KB
q80 639 KB
q70 584 KB
10x q90 761 KB
Three things to notice:
- q90 and q95 made it bigger, by 26% and 50%.
- Even q80 made it bigger. Only going below the original's quality actually saved bytes.
- Ten saves at q90 didn't keep growing. After the first re-encode the size settles. The quality loss from repeated saves is real, but it's mostly in the first generations.
Run it on your own photos. You'll quickly learn where "compression" stops paying for that kind of image.
The rule every compressor needs
Whatever the settings, a compressor must never hand back a bigger file than it was given:
const blob = await encode(file, { type: "image/jpeg", quality })
const grew = blob.size >= file.size
const result = grew
? { blob: file, size: file.size, grew: true } // keep the original, say so
: { blob, size: blob.size, grew: false }
And tell the user when it happens. "Already well compressed, kept the original" is useful information. A silent "-0%" or a larger download is a bug.
What actually reduces a JPEG
If the quality is already low, re-encoding won't save much. These do:
- Fewer pixels. A 4000 px photo shown at 1200 px on a page wastes most of its bytes. Resizing usually saves more than any quality setting.
- Strip metadata. EXIF, embedded thumbnails and colour profiles can add tens of KB, and EXIF may include GPS location.
- Chroma subsampling (4:2:0). Most photo encoders use it by default; check yours does for photos.
- A modern format where you control the delivery, like WebP or AVIF on your own site.
And keep the original. Every lossy save throws detail away for good, so compress from the master copy, not from yesterday's compressed export.
Try it on your photos
BatchSet's free JPEG compressor follows these rules: you pick a quality, it shows the size change on every file, and when a re-encode would be bigger it keeps your original and says so. It runs in the browser, so the photos aren't uploaded.
This article was drafted with an AI assistant. The script was run before publishing; the numbers shown come from a synthetic test image, as stated.
Top comments (0)