“Compress this image” is four distinct operations wearing one button. Knowing which of them costs you quality makes the settings far less mysterious.
Step one: decoding
The compressed file is turned back into a grid of pixels. Browsers do this in native code, with hardware acceleration where the platform offers it, and they handle JPEG, PNG, WebP and AVIF — plus HEIC on Apple platforms, because the operating system already ships that decoder.
Decoding is lossless with respect to the file: you get exactly the pixels the file describes. The loss already happened, when whatever produced the file encoded it.
Step two: orientation
Phone cameras store the sensor’s raw orientation plus a separate EXIF tag saying how to rotate it. Software that ignores that tag produces sideways photos. Applying it means rotating the actual pixel grid before anything else touches it — after which the tag is no longer needed, which is part of why re-encoding drops metadata.
Step three: resampling
If the target size cannot be met at full resolution, the image is scaled down. This is where a lot of perceived quality is silently won or lost. A cheap box filter averages neighbouring pixels and softens everything; a Lanczos filter weighs a wider neighbourhood and keeps edges crisp.
Resampling is genuinely destructive — the discarded pixels do not come back — but it is often the least destructive way to reach a small target, because it removes detail the encoder would otherwise spend its entire budget failing to describe.
Step four: encoding
The lossy part. JPEG converts to a brightness-plus-colour representation, discards some colour resolution (the eye barely notices), splits the image into 8×8 blocks, transforms each into frequency components, and then quantises — rounding away the high-frequency detail. The quality parameter is the coarseness of that rounding. Everything after it is lossless entropy coding, which is why an optimised Huffman table or a progressive scan saves bytes for free.
Where WebAssembly comes in
The browser’s built-in encoder is a black box: you can ask for a quality number and nothing else. No optimised entropy tables, no choice of baseline or progressive, no control over the resampling filter, and no way to know how many bytes a setting will produce without trying it.
A codec compiled to WebAssembly gives all of that back, at the cost of the bytes you spend downloading it. ToolZool runs the whole pipeline that way — decode, orientation, resample and encode are all Rust compiled to WebAssembly, in a background worker — rather than handing the decode to the browser. Three reasons, all of them practical:
- The search needs many encodes. Reaching an exact size means encoding the same image five or six times. Owning the encoder is what makes that cheap and repeatable.
- A decoder has to be able to say no. A file that declares 60,000 × 60,000 pixels has to be refused before it allocates, and a truncated file has to fail rather than return half an image. The browser’s decoder makes its own decisions there; ours states the limits and enforces them.
- The same input has to give the same output. Browser decoders differ in their colour handling and in how they treat malformed files. A pipeline that owns both ends produces the same bytes on every device, which is what makes a result you can check.
The cost is honest: that engine is a download. It is fetched only once you pick a file, so reading a page like this one never pays for it.
Why the search matters more than the codec
Given a target file size, the difference between a good encoder and a great one is a few percent. The difference between guessing at a quality slider and searching for the right one is much larger, because a guess that lands at 60% of your allowance threw away a third of the quality you were entitled to. That is the whole idea behind compressing to an exact size, and hitting a 50 KB limit shows what the search buys you at the hardest end of the range.

