Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 7 min read

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 records

The 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.

The 25 CC0 test photos (Wikimedia Commons)

| # | 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 |

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.