With mozjpeg, progressive JPEG is 0.2% to 1.2% smaller than baseline for photos 320 px and wider, and 2.3% larger for a 160 px thumbnail. The big saving is mozjpeg itself: 17% at 1920 px. One trap: in sharp, mozjpeg: true writes a progressive file even when you pass progressive: false.
What are the two layouts?
A baseline JPEG stores the image in one pass, top to bottom. A progressive JPEG stores it in several scans, coarse to fine, so the whole frame appears early and sharpens. The pixels and the quality are the same. Only the order of the data, and how well it compresses, change.
The sharp output docs list progressive with a default of false. They describe mozjpeg: true as a shortcut for four settings: trellis quantisation, overshoot deringing, optimised scans and quantisation table 3.
What does sharp write for each option?
I resized one camera photo (4964 × 2782) to six widths, encoded each at quality 80, and read back isProgressive from the output.
import sharp from 'sharp';
const src = 'camera.jpeg';
const modes = {
'baseline': { progressive: false },
'progressive': { progressive: true },
'mozjpeg, baseline': { mozjpeg: true, progressive: false },
'mozjpeg (default)': { mozjpeg: true },
};
console.log(['width'.padEnd(6), ...Object.keys(modes).map((m) => m.padStart(19))].join(''));
for (const width of [160, 320, 640, 1280, 1920, 4964]) {
const resized = await sharp(src).resize({ width }).raw().toBuffer({ resolveWithObject: true });
const row = [String(width).padEnd(6)];
for (const opts of Object.values(modes)) {
const out = await sharp(resized.data, { raw: resized.info }).jpeg({ quality: 80, ...opts }).toBuffer();
const m = await sharp(out).metadata();
row.push(`${out.length}${m.isProgressive ? ' P' : ' B'}`.padStart(19));
}
console.log(row.join(''));
}
width baseline progressive mozjpeg, baseline mozjpeg (default)
160 3754 B 3959 P 3445 P 3445 P
320 10870 B 10937 P 9589 P 9589 P
640 31824 B 31608 P 27154 P 27154 P
1280 95335 B 95114 P 79671 P 79671 P
1920 185336 B 184585 P 153394 P 153394 P
4964 850624 B 852196 P 668029 P 668029 P
Look at the third column. I asked for mozjpeg: true, progressive: false and got a progressive file, byte for byte the same as plain mozjpeg: true. The optimised-scans setting appears to force progressive mode, and it wins over your flag.
Note: If you need a true baseline file with mozjpeg's other tools, spell the options out and set
optimiseScans: false.progressive: falsenext tomozjpeg: truedoes nothing.
So what is the real byte difference?
To isolate the scan layout, I kept mozjpeg's tools and switched only the layout:
const moz = { trellisQuantisation: true, overshootDeringing: true, quantisationTable: 3, optimiseScans: false };
const modes = {
'moz baseline': { ...moz, progressive: false },
'moz progressive': { ...moz, progressive: true },
'moz + optimiseScans': { mozjpeg: true },
};
// same loop as above
width moz baseline moz progressive moz + optimiseScans
160 3369 B 3652 P 3445 P
320 9611 B 9850 P 9589 P
640 27289 B 27719 P 27154 P
1280 80676 B 81629 P 79671 P
1920 155054 B 157288 P 153394 P
4964 675911 B 683920 P 668029 P
| Width | Moz baseline | mozjpeg: true |
Difference |
|---|---|---|---|
| 160 px | 3,369 | 3,445 | +2.3% |
| 320 px | 9,611 | 9,589 | -0.2% |
| 640 px | 27,289 | 27,154 | -0.5% |
| 1280 px | 80,676 | 79,671 | -1.2% |
| 1920 px | 155,054 | 153,394 | -1.1% |
| 4964 px | 675,911 | 668,029 | -1.2% |
Three things stand out:
- A progressive layout with the default scan script is larger than baseline at every width here: 157,288 against 155,054 bytes at 1920 px.
- The optimised scans are what make progressive win, by about 1% from 1280 px up.
- At 160 px, baseline wins by 76 bytes. A tiny thumbnail has too little data for extra scans to pay off.
What does mozjpeg cost in time?
The 1920 px encode, median of 5 runs, from a raw buffer:
baseline 12.3 ms (median of 5)
progressive 23.1 ms (median of 5)
moz baseline 60.8 ms (median of 5)
mozjpeg: true 103.9 ms (median of 5)
mozjpeg: true is about 8 times slower than a plain baseline encode, for a file 17% smaller (153,394 against 185,336 bytes). That is a good trade for images you encode once and serve many times. It is a poor one inside a request that encodes on every hit.
Which should you pick?
-
Stored images, 320 px and up:
mozjpeg: true. You get progressive rendering and the smallest file. -
Thumbnails under about 200 px: spell out the mozjpeg tools with
optimiseScans: false, progressive: false. My 160 px test saved 2.3%. - Encodes on the hot path: plain baseline. It is the fastest, and the bytes differ by under 1% from plain progressive.
The JPEG settings on Blinkimg's JPG compressor are measured the same way, and how we measure explains the method behind its published numbers.
How I measured
- Machine: Apple M1 Pro, macOS, 2026-10-17.
- Node v24.22.0, sharp 0.35.4, libvips 8.18.6, mozjpeg as bundled with sharp.
- Source:
image-compressor-sample-camera-photo.jpegfrom the Blinkimg site, 4964 × 2782. Each width is a sharp resize to raw pixels, then the encode, so every option sees identical input. - All encodes at quality 80, default chroma subsampling (4:2:0). Byte counts are exact and repeat across runs. Times are the median of 5.
What I did not test
Decode time and how fast the first progressive scan paints in a browser. Other quality levels and other photos.
I build Blinkimg, an image compressor and converter with a developer API. I wrote this article with help from an AI assistant (Claude), which ran the code on my machine; every number in it comes from those runs.

Top comments (0)