I run a couple of small tool sites on my own, and one of them has an image upload component I wrote a while back. Before a file goes up to my server, the browser shrinks it with a canvas and toBlob. The compression level is a range slider that defaults to 0.8. I clicked through it in Chrome a few times before shipping, saw smaller files, and moved on.
Later I sat down and measured the whole export path in three engines. That is when I found out the slider had never done anything. An <input type="range"> gives you its value as a string, and I was handing that string straight to toBlob.
The setup
Everything ran headless on the three engines bundled with Playwright 1.61.1: Chromium 149, WebKit 26.5 and Firefox 151. The sample is a 2000×1500 image I generated with a script (gradient, noise and some geometric shapes), not a photo from anyone. The control I used was ImgIng's JPG conversion with its slider at 80, run on the same sample in all three engines. Its JPG path is the browser's own canvas encoder, and in Chromium its output matched my direct toBlob(..., 0.8) call byte for byte at 446,131 bytes. That told me my harness was measuring the right thing.
What a string quality actually does
In Chromium, quality as the number 0.8 gives 446,131 bytes. The string '0.8' gives 957,988 bytes, which is 2.15 times larger. That second number is not random. It is byte-identical to the output at 0.92, which is Chromium's JPEG default. The string is treated as if you passed nothing at all.
Firefox is worse. The number 0.8 gives 473,089 bytes and the string gives 1,845,543 bytes, a factor of 3.90. Its default is also 0.92, and somewhere between 0.8 and 0.9 it switches chroma subsampling from 4:2:0 to 4:4:4, so the jump is bigger.
No exception, no console warning, and the blob looks perfectly normal. For a site that pays for storage and bandwidth by the gigabyte, a silent doubling is worse than a crash. A crash I would have fixed the same day.
Other bad values behave the same way
I also tried 1.5, -1, NaN, null and the string 'abc'. Every one of them produced the default output. Out-of-range numbers are not clamped to 0 or 1; they are simply ignored. PNG ignores quality entirely in all three engines, so dragging a slider on PNG export does nothing either.
Falling back to "no quality" is not neutral across engines. For WebP, Chromium's default is 0.8 and Firefox's is 0.92. The same call with no quality gave 404,244 bytes in Chromium and 1,034,932 bytes in Firefox, 2.56 times more. So a string quality does not just cost you the value you picked. It hands the decision to whichever default the engine happens to have.
What I changed
Two things. The slider value now goes through Number() and a range check before it reaches toBlob, and anything outside 0 to 1 throws. During development I also export the same image at 0.5 and 0.9. If the two files come out about the same size, quality is not getting through.
One thing I have not solved. Even with a correct number, the same 0.8 gives 1,126,619 bytes in WebKit 26.5 versus 446,131 in Chromium, about 2.53 times. The engines map the number differently, and I have not tested real Safari or iOS. For now I check the actual byte size after export instead of trusting the quality value.
If your upload code reads quality from a form control, log typeof on the value you pass to toBlob, then export one image at two quality levels and compare the sizes.
Top comments (0)