Notes › Tech Notes › Setting quality to 100 and compressing again changes nothing at all

Setting quality to 100 and compressing again changes nothing at all

Drag quality to the far right and it spits back the original file

I had to submit an image to a form with a hard size cap, and that image had already been compressed once. To squeeze out some margin, the obvious move was to drag quality to the far right: maximum quality, which by any logic should not be worse than the 80 setting. Actual run: an 800×600 photo input at 208986 B came out at 208986 B, saving 0 B.

Line 180 reads if (quality >= 1) {, and when it matches, the next step is item.blob = item.file;, with resultSize taken from item.file.size and not a single encode performed. That Lossless button (setQuality(100) on line 27) is not a maximum-quality setting; it is a passthrough of the original file.

Only two steps between the quality value and the encoder

The number is parsed in exactly one place, line 178: parseInt(value) / 100. parseInt runs before the division, so the decimal tail is cut first: 99.5 becomes 99 and 80abc becomes 80. Empty values and abc give NaN, and NaN >= 1 is false, so they fail the gate on line 180 and are handed to canvas.toBlob as-is. Chrome encodes such out-of-range values at its own default setting: NaN, -0.05 and 1.5 all produce 214532 B, between 204727 B at the 0.9 setting and 253301 B at 0.95.

There is no fallback at all on the code side: Math.min, Math.max and clamp appear nowhere, and [10,100] is held up entirely by the range control. Writing a value and reading it back shows how much the control shields the code: 101 to 100, -5 to 10, abc to 55, 55.5 to 56, 1e2 to 100.

Why the high-quality settings push the size back up

The same 800×600 composite photo, measured setting by setting:

SliderEncoder qualityOutput bytesVersus original
800.8143525 B31.3% saved
900.9204727 B2.0% saved
990.99333872 B59.8% larger
100No encode208986 B0 B
80 setting 143525 B 90 setting 204727 B 99 setting 333872 B past the reference line 100 setting 208986 B passthrough Original 208986 B

Figure 1 | Measured bytes for the 800×600 composite photo; the bar at 100 is not an encoding result but the input passed through untouched.

More striking is a small image that had been compressed hard: 851 B at 64×48 (the product of the 0.1 setting) comes out at 1284 B on the 99 setting, up 433 B or 50.9%. The size rollback exists only in the PNG branch (line 205); the else branch shared by JPEG and WebP compares sizes in 0 places, so the larger result still becomes the download. I ran this set of settings on the JPG compression page, and the byte counts replay exactly.

The PNG path runs a JPEG pass first

The PNG branch takes a lossy detour: line 197 sets adjustedQuality = quality * quality, line 198 encodes the canvas as JPEG, and the result is drawn back to canvas and output as PNG. The second toBlob on line 214 passes only 2 arguments, so the PNG encoder never receives quality at all.

Original PNG 2597 B Intermediate JPEG 0.64 2070 B PNG encode, no quality 8531 B Line 205 rollback 2597 B Transparent pixels alpha 5013 → 0 Transparent area luminance 0 → 1; UI shows only no change

Figure 2 | Byte chain for a 240×240 transparent logo, measured with a composite image in headless Chrome.

larger_than_input is recorded as yes on this chain, and line 205 grabs the original bytes back at that point. Transparency is also lost on the detour: 5013 fully transparent pixels become 0, and the average luminance of the transparent area goes from 0 to 1, which amounts to filling it with opaque black. A PNG without transparency grows too, 2114 B to 3340 B, and also ends with the rollback.

What gets lost is metadata; what stays put is the dimensions

For a sample with APP1 inserted into a JPEG by hand, the input side shows yes for marker_FFE1, Make and the GPS sentinel, and after re-encoding all three turn no, with bytes dropping from 2299 B to 1794 B. The Orientation=6 image is natively 64×48, and what the page receives from decoding is already 48×64; the rotation happens before the template. The output is always JFIF as written by Chrome: units=0, xd/yd at 1×1, and values such as the original 300dpi no longer exist (a camera original was not tested).

Dimensions are stable: both drawImage calls use the three-argument form, with 0 scaling or resampling, and resize and scale match 0 times in the template. 800×600 in gives 800×600 out, and after double PNG encoding it is still 240×240. Changing resolution requires a different route; this note on resizing matches that need.

"Saved" is not the same calculation in two places

The single-item test is originSize > resultSize, strictly greater, so only then does it write "-63.9 KB"; equal and larger sizes both fall into "no change", and the difference never appears. Batch mode is another set: line 252 has saved = totalOrigin - totalResult, line 258 wraps the byte count in Math.abs(saved), turning a negative saving into a positive number, while the pct percentage is not absolute-valued, so the minus sign stays and the same row reads "saved: 433 B (-50.9%)". The denominator is wider too: line 250 sums origin across all files, and line 251 substitutes originSize for the ones that were not compressed, so the percentage is naturally lower than intuition suggests.

Batch mode has only two gates, and downloads are not zipped

There is no file-count ceiling: no MAX, no length truncation. The only gates are the 20MB per-file limit on line 77 (rejected only when strictly greater; exactly 20971520 B is allowed) and the MIME whitelist on line 73. That regex is not anchored: image/jpg, gif, heic and uppercase IMAGE/PNG are all rejected, while data/image/pngx gets through and is handed to the encoder as its mime type unchanged.

"Compress all" is the for...of loop with await on line 240, fully serial, with 0 concurrency and 0 progress bar. Batch download is N independent triggers, and line 280 has setTimeout(..., i * 300), hard-coding the cadence at 300ms. zip and JSZip match 0 times in the template, so nothing is packaged. Filenames only go through the regex on line 268, turning name into name_compressed.ext; the Date.now() plus 4 random digits id on line 87 is only a DOM key and never enters the download name, so a noext file is copied as-is, two files named noext cannot be told apart, and a.tar.gz only changes the last segment.

Q: Is WebP much smaller than JPEG?

At the same 0.8 setting the two rows output JPEG 143525 B and WebP 140356 B, a 2.2% difference. But the two inputs do not share a source (208986 B versus 151210 B), and the bytes come from a synchronous toDataURL, so 2.2% only indicates magnitude; the 25-35% claimed in marketing cannot be confirmed from this sample. WebP shares the same else branch as JPEG and has no dedicated parameters, though transparency is preserved: 5013 to 5013.

Q: Should the Lossless button be used at all?

Only when you do not want any re-encoding, because what it gives you is the original file. If the goal is a smaller file, the 99 and 100 settings are no help: one pushes 208986 B up to 333872 B, the other does not change a byte, and neither qualifies as nearly lossless. In the code, 101 and 150 pass through as well; the slider just clamps at 100 so you cannot reach them.

Q: When should this page not be used?

When you need to keep EXIF, GPS or camera information, when you need to keep a transparent PNG background, or when you need to change resolution. It does none of these three, and the four entry points in this family share the same template, so switching entry points does not change the result; for the general trade-offs, see this compression guide.

📎 Related articles

PNG compressed three times only lost 426 bytes: the real boundary of lossless compression 2026-10-22 How Many Times Bigger a PNG Gets After Converting From WebP: When It Is Unavoidable and How to Keep the Growth Down 2026-10-22 Three Reasons the Small Text Smudges When a Screenshot Is Shrunk to a Quarter 2026-10-22 Cropped to the Same 300x300, Why Does the Avatar Always Sit Half a Cell Low-Right 2026-10-22 Forecasting 18 months at 1.7 times a month: why the script and the sheet give two numbers 2026-10-22