Notes › Tech Notes › How Many Times Bigger a PNG Gets After Converting From WebP: When It Is Unavoidable and How to Keep the Growth Down

How Many Times Bigger a PNG Gets After Converting From WebP: When It Is Unavoidable and How to Keep the Growth Down

A designer ships a stack of WebP files and the receiving side only accepts the old bitmap formats, so the only option is PNG. After conversion the dimensions are unchanged and the transparency is still there; the only thing that moved is the file, from a few hundred bytes to tens of thousands. The worst one went from 358 B to 28946 B, up 80.8547 times. I assumed the tool had damaged the images. It took comparing pixels to see that what had broken was my idea of lossless: PNG promises the pixels will not change, it never promises a byte count.

First the 13 measured runs: from 1.4194 times to 80.8547 times

I put the whole batch through WebP to PNG one file at a time and picked 4 that cover different kinds of content:

SampleInputOutputRatio
flat_ui_1440_lossless.webp358 B28946 B80.8547×
gradient_256_q80.webp826 B64408 B77.9758×
lossy_photo_400_q80.webp1996 B107853 B54.0346×
one_px.webp62 B88 B1.4194×

The 1×1 one_px only pays 26 B of container overhead. In the 1440×900 flat colour UI, a lossless WebP spends 358 B to write down 6 colours, while handing those pixels to an RGBA container costs 28946 B. For the 8 file batch the summary bar reports that 8 / 8 files converted and that a total of 17.5 KB became 499.8 KB, the size being up 2764%, and that figure comes from weighting the 17872 B of input against the 511807 B of output, 28.6373 times, not from averaging the ratios of the individual files.

What grows is the encoding, not the picture

In the 5 pairs where I decoded the source and the result and compared them, diffPixels = 0, maxChannelDelta = 0 and totalAbsDelta = 0, and the colour counts match digit for digit: 23511 → 23511 and 6 → 6. So the only thing that can have grown is the way the pixels get described. The output is always RGBA at colour type 6, the chunk list is only IHDR, IDAT and IEND, there is no pHYs and no tRNS, no palette is built, and every pixel costs 4 bytes: the 1440×900 flat field spreads out to 5184000 bytes of raw RGBA and still needs 28946 B once it is compressed.

The contrast runs against intuition. Lossless WebP grows the most gently, 12938 B → 43232 B is only 3.3415 times; the q80 photo explodes hardest, 1996 B → 107853 B is 54.0346 times; and for one and the same text screenshot, lossless is 15.4036 times while q80 is only 4.6521 times. WebP throws the high frequencies away first and gets the bitstream down to a few hundred bytes, then PNG re-lays every pixel at 4 bytes regardless. And diffPixels = 0 only guarantees that no new loss is added: after the conversion the colour count is still 28504, the banding is pinned exactly as it was, and line 234 holds a single drawImage call with no dithering.

flat_ui_1440_lossless 80.8547× gradient_256_q80 77.9758× lossy_photo_400_q80 54.0346× large_1600_q80 23.8811× lossless_photo_400 3.3415× one_px 1.4194×

Figure 1 | Every bar is output bytes divided by input bytes: they are all described as growth, yet a flat UI, a gradient and a lossy photo are nowhere near the same scale.

When the conversion really is necessary

  • The receiving side cannot decode WebP. What comes back is an ordinary PNG with IHDR, IDAT and IEND; for 13/13 files the width and height equal the input size, and across 12 sizes from 1×1 to 1600×1200 nothing is resampled.
  • The transparency still has to stay editable downstream. The checkbox that preserves the alpha channel is forced on and locked, and 48635 fully transparent, 4641 semi-transparent and 36724 opaque pixels go into the PNG unchanged; the very same image written as JPG is 9633 B, with every transparent pixel filled in by #ffffff.
  • The image is an input to an algorithm and you have to sample it pixel by pixel: interlace 0, RGBA at 4 bytes per pixel, and the values you read back differ from the WebP decode by diffPixels = 0.
  • If you only want a smaller page, do not convert at all, because 13/13 files got bigger and the smallest still grew by 1.4194 times. To actually save bytes you have to go the other direction, and only the PNG to WebP side has a quality slider.

How to keep the growth down

Cross one road off first: under the PNG tab the entire quality row is display=none, toBlob receives quality as undefined, dragging the slider to 10 changes nothing but the label, and converting again returns exactly the same byte count. There is also no size ceiling and no compression switch; a static scan for MAX and slice(0, N) hits 0 times, so the 10718 B large_1600_q80 converts straight into 255958 B.

What you can still move is the content and the container. Encode the same pixels again with a different encoder and RGBA against the reference output is 64408 versus 571, 28946 versus 5524, 4459 versus 1421, and 50000 versus 39338.

112.8× tool 64408 B reference 571 B 5.24× tool 28946 B reference 5524 B 3.14× tool 4459 B reference 1421 B 1.27× tool 50000 B reference 39338 B

Figure 2 | The dark and light bars share one scale; the gap comes from an encoder that builds no palette, not from a setting you are allowed to tune.

64408 versus 571 is 112.8 times apart, so most of that growth is RGBA tiling redundancy, and swapping the container beats swapping a parameter; 50000 versus 39338 is only 1.27 times apart, which tells you the bulk of it is real information and cannot be squeezed out. If the deliverable has to be PNG, run the result through a PNG compression that only touches the filters afterwards. If the receiving side never said it refuses WebP, do not take this detour at all.

A few failures that happen without a sound

  • An animated input yields one frame only: animated_2frame.webp goes 338 B → 1045 B and the same asset as GIF goes 597 B → 1028 B, both a single 200×120 frame, the chunks carry no acTL, and the frame delay and the loop count are gone.
  • Pixels at alpha = 0 have their RGB cleared: an input of (255,0,0,0) comes out as (0,0,0,0), the 57600 pixels collapse to a colour count of 1, and 580 B grows to 2072 B.
  • There is no format whitelist, accept only says image/*, so 39587 B of JPEG data named fake_webp_is_jpeg.webp still decodes to 1200×900 and produces 504277 B; only notes.txt is refused, and then the notice just names it as a skipped non-image file.
  • The ZIP depends on the jszip 3.10.1 script pulled from cdnjs on line 57: with network access the 8 files come back as a 512759 B ZIP, 952 B above the 511807 B sum of its members; when that script cannot be fetched, the button quietly degrades to downloading only the first file, the 88 B one_px.png, with no error text at all.

Q: Is all this growth a sign that the conversion broke something?

No. diffPixels = 0 and maxChannelDelta = 0, not one colour count changed, and for 13/13 files the IHDR width and height also equal the input. Everything that grew belongs to the encoding: 4 bytes per pixel and no palette, so 1.4194 times and 80.8547 times are the two ends of one rule.

Q: Can the tool be made to grow less?

It cannot. The quality row is hidden and quality is handed over as undefined, so dragging to 10 leaves the output unchanged. There is no size ceiling and no branch that falls back to the original format when the file gets larger, because lines 224 through 236 only run drawImage plus toBlob. The only knobs left are the format you settle on before converting and a compression pass afterwards.

Q: For this batch, convert or compress?

First decide whether the problem is that the receiver does not support WebP or that the size is unsatisfying. The first one has to convert even though all 13 groups came out bigger; the second only gets worse after a conversion. When you are unsure, estimate from the content: a flat UI lands at 80.8547×, a q80 photo at 54.0346×, a lossless photo at 3.3415×.

The batch was converted in the end. I only pasted the line where 17872 B became 511807 B, up 2764%, into the requirements sheet. What actually changed was the order of work: the delivery format gets fixed by the receiving side before anyone starts, instead of being discovered after a conversion that nothing fits.

📎 Related articles

PNG compressed three times only lost 426 bytes: the real boundary of lossless compression 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 Which values quietly change when you paste API JSON into a PHP array 2026-10-22