Why a canvas re-encode can make a JPEG bigger
Disclosure: This report on the image-processing tests run by the LighterPics project was written with AI assistance. Exporting an existing JPEG through a canvas does not guarantee a smaller file. At quality 0.92, five o
Disclosure: This report on the image-processing tests run by the LighterPics project was written with AI assistance.
Exporting an existing JPEG through a canvas does not guarantee a smaller file. At quality 0.92, five of 25 test photos grew in Chromium, 10 in Firefox and 11 in WebKit.
| Engine | Photos larger than the input | Median output size, as a percentage of input size |
|---|---|---|
| Chromium 145.0.7632.6 | 5 of 25 | 60% |
| Firefox 146.0.1 | 10 of 25 | 84% |
| WebKit 26.0 | 11 of 25 | 80% |
All three ran the same photos at the same requested quality. For every photo, the Firefox and WebKit JPEGs were larger than the Chromium JPEG. The quality argument alone was not enough to predict the output size across engines.
What changes when a JPEG is re-encoded
A canvas export starts from decoded pixels and creates a new file. Its size depends on the output format, encoder settings and image content. The API does not promise to keep it below the input byte count.
The converter used createImageBitmap() followed by a canvas export. Its quality settings were passed to toBlob() or the equivalent worker export; PNG does not use that argument. [1]
Higher quality often meant a larger file
Chromium ran the wider quality sweep. Here are the results for the 25 JPEG photos:
| Output setting | Photos that grew | Median size relative to input | Range |
|---|---|---|---|
| JPEG 1.0 | 23 of 25 | 197% | 91% to 586% |
| JPEG 0.92 | 5 of 25 | 60% | 20% to 138% |
| JPEG 0.80 | 2 of 25 | 39% | 8% to 106% |
| WebP 1.0 | 25 of 25 | 250% | 141% to 813% |
| WebP 0.92 | 3 of 25 | 50% | 10% to 152% |
| PNG | 25 of 25 | 483% | 224% to 1,267% |
At JPEG 0.70, no photo grew and the median output size was 26% of the input. No photo grew at any of the lower tested JPEG quality values either.
At JPEG quality 1.0, the median output was nearly twice the input size. That setting requests the highest quality, but it still re-encodes the decoded image and cannot recover detail lost in the original JPEG. The recorded median PSNR, a measure of pixel error against the decoded input, was 56.88 dB at 1.0, 43.33 dB at 0.92 and 40.29 dB at 0.80. These were objective measurements, not a visual preference test.
WebP at 1.0 took a different path in this Chromium build: all 25 files contained the lossless VP8L bitstream. At 0.92, they contained lossy VP8. The WebP container specification distinguishes these two formats. [2]
Firefox also produced three larger files out of 25 at WebP 0.92, with a median size of 50% of the input. WebKit produced no accepted WebP output. PNG made every photo larger in all three engines; the Firefox and WebKit medians were 383% and 421%, respectively.
The five photos that grew at JPEG 0.92 in Chromium also grew in Firefox and WebKit. Four were night shots and one was a portrait. They were among the inputs with the fewest bytes per pixel, but that alone did not explain the result: the lowest-bytes-per-pixel photo shrank in Chromium. The tests did not record encoder libraries or chroma subsampling, so they do not identify the cause of the engine differences.
A size target did not guarantee a smaller file
The compressor searched for an output that fit a requested byte budget. Across 1,407 Chromium runs that produced a file, it returned 177 files larger than their inputs. Every one had a target at or above the input size.
| Target relative to input size | Runs with an output | Output larger than input | Target met | Target missed |
|---|---|---|---|---|
| Below input size | 1,038 | 0 | 773 | 265 |
| At or above input size | 369 | 177 | 369 | 0 |
A re-encoded file can fit the requested budget while still exceeding the original size.
In JPEG mode, the compressor first tries quality 0.92. If that output fits, it returns it; otherwise it searches lower qualities and, when allowed, reduces the pixel dimensions. The WebP and MozJPEG traces also started at 0.92 for the runs that produced larger files.
Of the 177 larger outputs, 159 came from the first encode at 0.92. The other 18 came from searches that settled between 0.75 and 0.91. All met the target, but none reduced the input byte count.
Reducing pixel dimensions helped when the target was below the input size: the compressor met the target in 543 of 549 runs with resizing allowed, compared with 230 of 489 without it. Successful runs in that group reached a median of 98.4% of the target. When the target was missed, the tool returned its smallest attempt; those outputs still did not exceed the input size in this test set.
For the single cross-engine compressor setting, JPEG with a 200 KB target and resizing allowed, all 25 photos came in under the target in every engine. That result does not extend to all targets or settings.
If a tool must never increase file size, it needs a separate comparison against the input byte count. Checking only the requested target is insufficient.
The starting format also mattered
Converting PNG to JPEG was not a reliable way to shrink the generated screenshots and graphics. At JPEG 0.92 in Chromium, four of five screenshots grew, with a median output size of 122% of the input. Three of five graphics grew, with a median of 110%.
A separate test first converted each of the 25 photos to WebP at 0.80. Those WebPs were a median of 23% of the original JPEG size. Converting them again made every file larger than its intermediate WebP:
| Conversion from WebP 0.80 | Median size relative to WebP | Range |
|---|---|---|
| JPEG 0.92 | 2.31Γ | 1.59Γ to 3.37Γ |
| JPEG 0.80 | 1.49Γ | 1.02Γ to 1.88Γ |
| PNG | 18.41Γ | 9.72Γ to 25.87Γ |
These ratios use the intermediate WebP as the reference file.
Reproduce one example
This short example uses one of the photos that grew at JPEG 0.92. Open about:blank in a desktop browser and paste it into the DevTools console. The Wikimedia file server allows the cross-origin request.
(async () => {
const src = await (await fetch("https://upload.wikimedia.org/wikipedia/commons/0/0f/Star_night_%28Unsplash%29.jpg")).blob();
const bitmap = await createImageBitmap(src, {imageOrientation: "from-image"});
const canvas = document.createElement("canvas");
canvas.width = bitmap.width; canvas.height = bitmap.height;
canvas.getContext("2d").drawImage(bitmap, 0, 0);
for (const [type, quality] of [["image/jpeg", 1], ["image/jpeg", 0.92], ["image/jpeg", 0.8], ["image/png"]]) {
const out = await new Promise(resolve => canvas.toBlob(resolve, type, quality));
console.log(type, quality ?? "", out.type, out.size, (100 * out.size / src.size).toFixed(0) + "% of the original");
}
})();
The input is 878,879 bytes. The recorded outputs were:
| Output | Chromium output, in bytes |
|---|---|
| JPEG 1.0 | 4,145,012 |
| JPEG 0.92 | 1,140,171 |
| JPEG 0.80 | 798,492 |
| PNG | 8,707,460 |
At JPEG 0.92, Firefox produced 1,557,092 bytes and WebKit 1,690,770 bytes.
In the recorded snippet check on 2026-11-11, every output size with a harness counterpart matched: all four settings in Chromium, and JPEG 0.92 and PNG in Firefox and WebKit. Different browser builds may produce different sizes.
Test setup and limits
The harness built LighterPics locally and called the processing functions and Web Workers used by its tool pages. The runs used Playwright 1.58.2 on an Apple M3 Pro, macOS Darwin 25.6.0, arm64. The engines were the headless Playwright builds named above, not the corresponding branded releases. [3]
Inputs were 25 CC0 JPEG photos from Wikimedia Commons, checked against their SHA-256 hashes, plus five generated PNG screenshots and five generated PNG graphics for the Chromium tests. The photos ranged from 316,480 to 5,603,777 bytes and from 3.15 to 12.19 megapixels.
Chromium ran 10 JPEG and 10 WebP quality settings plus PNG. Firefox and WebKit ran JPEG 0.92, WebP 0.92 and PNG. The compressor sweep was Chromium-only: seven targets from 20 to 2,000 KB, with resizing enabled and disabled, using canvas JPEG, canvas WebP and WebAssembly MozJPEG. Here, 1 KB means 1,000 bytes. An additional 63 MozJPEG runs failed on the pixel limit with resizing disabled and are excluded from the 1,407 runs with output.
The project page publishes selected Chromium summaries. The cross-engine results and some other figures in this article are held only in the private result files listed below. [4]
These are observations from one machine and a small input set. Other browser versions, operating systems and photos may produce different results. There was no human assessment of visual quality, and matching a size target should not be read as preserving acceptable image quality.
Run recordsThe main Chromium run used site and harness commit eacd79c, harness version 1.1.0, on 2026-11-10. The WebP-then-convert run used harness 1e3cf33 with the same site code. The cross-engine run used harness 349005d, version 1.2.0, on 2026-11-11.
The results are stored in results-2026-11-10-r5.json, results-2026-11-10-webp.json and results-2026-11-11-metadata.json. The WebP-then-convert run and cross-engine run have repeat files; the main Chromium run was not repeated at this code version. The repository and raw results are private.
| # | File on Commons | Author as recorded on Commons | Licence | Pixels | Bytes |
|---|---|---|---|
| 1 | Π‘ΠΎΠ²Π΅ΡΡΠΊΠΈΠΉ ΠΡΠ΄ΠΈΠ»ΡΠ½ΠΈΠΊ 12Ρ34ΠΌ.jpg | Petra Dr | CC0 | 2896Γ1944 | 2,186,980 |
| 2 | Alone in the unspoilt wilderness (Unsplash).jpg | David Marcu davidmarcu | CC0 | 3506Γ2329 | 3,655,400 |
| 3 | Brickwall (Unsplash).jpg | Namroud Gorguis namroud | CC0 | 2784Γ1856 | 5,603,777 |
| 4 | Coffee at work (Unsplash).jpg | Cathryn Lavery cathrynlavery | CC0 | 2400Γ1600 | 985,861 |
| 5 | Friends 1 (Unsplash).jpg | Joel Mwakasege joelmwakasege | CC0 | 2592Γ1933 | 316,480 |
| 6 | Green grass texture (Unsplash).jpg | Marc Wilnauer wk25 | CC0 | 3232Γ1824 | 3,497,050 |
| 7 | Healthy Cafe Food (Unsplash).jpg | Jade Wulfraat jadew | CC0 | 2936Γ1960 | 2,812,198 |
| 8 | Kolkata City at night wide view.jpg | XYZ 91973 | CC0 | 8632Γ864 | 533,436 |
| 9 | Landscape-mountains-clouds-trees (24326873785).jpg | www.Pixel.la Free Stock Photos | CC0 | 5184Γ1520 | 2,864,749 |
| 10 | Leaf texture in macro (Unsplash).jpg | Vadim Gromov motioq | CC0 | 3885Γ2590 | 3,557,162 |
| 11 | Lighthouse at night (Unsplash).jpg | Janus Y janus | CC0 | 3888Γ2592 | 2,563,831 |
| 12 | Log pile in close-up (Unsplash).jpg | Sven Scheuermeier sveninho | CC0 | 2560Γ1707 | 2,616,623 |
| 13 | Man with tribal face paint (Unsplash).jpg | Scott Umstattd scott_umstattd | CC0 | 3464Γ2309 | 4,303,508 |
| 14 | Marmolada, Italy.jpg | Marco Bonomo | CC0 | 3904Γ2604 | 3,396,302 |
| 15 | Organic Carrots (Unsplash).jpg | Harshal Hirve harshalhirve | CC0 | 4160Γ2416 | 2,392,141 |
| 16 | Pieces of Watermelon (Unsplash).jpg | Neha Deshmukh nehadeshmukh | CC0 | 2000Γ3000 | 3,070,980 |
| 17 | Portrait of Prof Mikhail Lemeshko.jpg | not recorded | CC0 | 2034Γ2717 | 742,583 |
| 18 | Rangpur city highway at night 1.jpg | Cerium4B | CC0 | 2048Γ1536 | 450,984 |
| 19 | Singapore night (Unsplash).jpg | Pierpaolo Lanfrancotti goofy1981 | CC0 | 4153Γ2535 | 1,122,928 |
| 20 | Small and medium pumpkins (Unsplash).jpg | Corey Blaz blazphoto | CC0 | 4032Γ3024 | 2,285,378 |
| 21 | Smiling Eyes (Unsplash).jpg | Luca Campioni white73widow | CC0 | 2136Γ1536 | 981,696 |
| 22 | Star night (Unsplash).jpg | Pasi Jormalainen paatse | CC0 | 2488Γ1558 | 878,879 |
| 23 | Strong woman (Unsplash).jpg | Scott Webb scottwebb | CC0 | 4190Γ2782 | 3,846,153 |
| 24 | Woman watching the sunset (Unsplash).jpg | Lucas Gallone lucasgallone | CC0 | 3264Γ1836 | 1,491,446 |
| 25 | Woman with a pink flower (Unsplash).jpg | Camila DamΓ‘sio miladamasio | CC0 | 3331Γ2221 | 4,663,027 |
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.