Resizing an image sounds like one operation. It is usually three, and the expensive one is the part nobody thinks about: turning the compressed file back into pixels before you touch it.
The arithmetic of a thumbnail
Take a 6000 × 4000 photograph — an ordinary size out of a modern camera — and ask for a 300 px thumbnail. The naive path is:
- decode the JPEG to a full-size bitmap;
- resample that down to 300 px;
- encode the result.
Step one produces 6000 × 4000 = 24,000,000 pixels. At three bytes each that is
roughly 72 MB of memory, materialised in full, so that you can throw
away 99.8% of it a moment later. The thumbnail you actually wanted is
300 × 200 = 60,000 pixels.
JPEG lets you skip most of it
This is a property of the format rather than a trick. JPEG stores an image as
8 × 8 blocks of discrete cosine transform coefficients, and a decoder can
reconstruct each block at reduced size by using only the low-frequency coefficients. The scale
factors that fall out of that structure are simple fractions — 1/2,
1/4, 1/8 — and the decoder never has to build the full bitmap at all.
For our thumbnail, 6000 / 8 = 750, which is still comfortably above the
300 px we need. So decode at one eighth and you get a
750 × 500 bitmap — 375,000 pixels instead of
24,000,000. That is 64 times fewer, and the arithmetic is
just 8 × 8: the saving is quadratic because you are scaling both axes.
You then resample 750 px down to 300 px normally. Quality is preserved because you are still downscaling from more data than you need — which is exactly the condition under which resampling looks good.
Where it does not apply
Three limits, all worth knowing before you assume it is free:
- JPEG only. The trick is a consequence of the DCT block structure. PNG is a filtered bitstream and WebP and AVIF are block-based codecs with their own rules; none of them offers the same cheap scaled decode.
- Downscaling only. Asking for an output larger than the source means you need every pixel there is.
- Never below your target. Pick the largest scale factor whose result is still at or above the size you want. Decode to 375 px when you need 300 and the result is fine; decode to 187 and you are upscaling, which looks it.
The trap: your size guard now reads the wrong number
This is the part that bites, and it is a security bug rather than a performance one.
Any service that decodes untrusted images needs a limit on how large an input it will accept — otherwise a small file that expands to an enormous bitmap, a decompression bomb, is a trivial way to exhaust your memory. The usual implementation reads the decoded image and refuses it if it is too many megapixels.
Introduce scaled decoding and that check quietly stops working. The decoder now hands you the shrunken image, so a 200-megapixel input arrives as a tidy few megapixels and sails past the guard it was supposed to trip.
The fix is to read the header first. Dimensions are in the file's metadata, available before any pixels are reconstructed, so the limit can be applied to what the file claims rather than to what you were handed. Check the header, refuse early, and only then decide what scale factor to decode at.
Worth measuring yourself
The size of the win depends entirely on the ratio between your sources and your outputs, so the honest advice is to measure your own traffic rather than trust a figure from somebody else's. If your originals are already close to the sizes you serve, there is nothing here for you. If you are generating thumbnails from camera-sized photographs, the pixels you never decode are the cheapest ones you will ever not process.