Skip to content
Back to Blog
Tutorials

Image Compression Not Working? Why Files Get Bigger

A compressor that makes files bigger isn't broken. The browser canvas re-encodes to 32-bit RGBA. We measured 83 real PNGs: all 83 grew. Try our free in-browser compressor.

14 min read

Image Compression Not Working? Why Files Get Bigger

Your compressor is probably fine. When image compression is not working, and the output comes back the same size or noticeably bigger than what you fed in, the cause is almost always one of three things. A broken tool is not one of them.

The most common one: any tool that compresses through the browser’s canvas element discards your PNG’s color type and re-encodes every pixel as 32-bit RGBA. We pushed all 83 Open Graph card PNGs from this site through canvas.toBlob('image/png') in Chrome 151.0.0.0. Every one of the 83 came back larger. Median growth was +76.6%, the smallest was +35.6%, the largest +237.2%.

The second cause: your JPEG is already compressed. Run it through five rounds at quality 0.8 and the file size stops moving after the second pass, while every pass keeps degrading the picture.

The third: your PNG has already been quantized. There is no color redundancy left for a second pass to remove.

None of these is fixed by dragging the quality slider lower. The fix is a different format or a different encoder. Those same 83 PNGs converted to WebP at quality 0.8 all got smaller, median −94.2%.

How these numbers were produced: Chrome 151.0.0.0 driven by Playwright, macOS 26.5.2, Node v25.8.2, upng-js 2.1.0, ImageMagick. The 83-file set is every PNG in this site’s public/og directory, not a sample of it. Four additional controlled files at 1200×630 and 800×600 cover the photo, graphic, palette and JPEG cases separately.

1. Image compression not working: a thirty-second triage

Find your row, then read the section it points at.

What you fed inWhat you gotRoot causeRead
PNG screenshot or graphicBigger than the originalcanvas re-encoded it as 32-bit RGBASection 2
JPEG photoBigger than the originalIt was saved as a PNGSection 2
JPEG photoBarely any changeAlready at its size plateauSection 3
PNG already run through a compressorNo change, or slightly biggerNo headroom leftSection 4
AnythingSmaller, but soft or off-colorGeneration loss, or a stripped ICC profileSections 3 and 6

The rows are not mutually exclusive. A JPEG photo dropped into a canvas-based PNG exporter hits the first two at once, which is how a file of 47,828 bytes turns into one of 809,415 bytes.

If you want to skip the diagnosis and just get a smaller file, our image compressor runs a quantizing PNG encoder rather than a canvas round-trip, and it discards its own output whenever the result is not smaller than what you uploaded.

2. Cause one: the browser canvas always writes 32-bit RGBA

What canvas.toBlob() does to your PNG

A canvas round-trip has no compression step. It decodes and re-encodes, and everything that is not a pixel value gets dropped in between.

Draw an image onto a canvas and the browser decodes it into a flat RGBA buffer of four bytes per pixel, with the palette, the bit depth choice and the metadata all discarded. HTMLCanvasElement.toBlob() then encodes that buffer from scratch. The PNG specification defines six color types, and a PNG encoder is free to pick the cheapest one that represents the image. Chrome’s canvas encoder does not pick. It always emits color type 6.

Color type going inWhat canvas.toBlob('image/png') gives back
RGB (color type 2)RGBA (color type 6)
Palette (color type 3)RGBA (color type 6)
JPEG (has no PNG color type)RGBA (color type 6)

You can read this out of the bytes directly. Byte 25 of a PNG file is the bit depth and byte 26 is the color type, both inside the IHDR chunk:

xxd -s 24 -l 2 -p suspect.png
# 0806  ->  bit depth 8, color type 6 (RGBA)

All three input types above produced depth=8 type=6. A 64-color palette image stores one byte per pixel plus a small table; after the round-trip it stores four bytes per pixel and the table is gone. Deflate recovers some of that, never all of it.

Reproduce it in your own browser with this console snippet. It picks a file, round-trips it, and prints both sizes plus the color type:

const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/png,image/jpeg';
input.onchange = async () => {
  const file = input.files[0];
  const bitmap = await createImageBitmap(file);
  const canvas = document.createElement('canvas');
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  canvas.getContext('2d').drawImage(bitmap, 0, 0);
  const blob = await new Promise((r) => canvas.toBlob(r, 'image/png'));
  const head = new Uint8Array(await blob.slice(0, 26).arrayBuffer());
  console.log(file.name, file.size, '->', blob.size);
  console.log('bit depth', head[24], 'color type', head[25]);
};
input.click();

The quality argument to toBlob is ignored for image/png. PNG is lossless, so there is nothing for a quality number to trade away, and a slider that appears to control PNG compression in a canvas-based tool is controlling nothing.

83 files measured: every compressed image larger than the original

The full run, not a selected subset of it:

MeasurementValue
Files tested83 (every PNG in public/og, both RGB and palette color types)
Files that grew83 / 83 (100%)
Smallest increase+35.6%
Median increase+76.6%
Largest increase+237.2%

One file, so you can see the shape of it: aes-decrypt.png went from 505,516 B to 898,014 B, an increase of +77.6%. The same file encoded as WebP at quality 0.8 is 27,188 B, −94.6%.

The sample boundary matters here. These 83 files are Open Graph cards: 1200×630, flat backgrounds, large text, a handful of brand colors. They are exactly the kind of graphic that a good PNG encoder handles well, which is why a canvas round-trip hurts them so much. This is a strong result for graphic-type PNGs; it is not a claim that every PNG in the world grows on a canvas round-trip. A photographic PNG that was already stored as full RGBA has much less to lose.

What it does establish: if your compressed image is larger than the original and the tool runs in a browser tab, the encoder is the first place to look, not your settings.

Why a JPEG saved as PNG grows 16.9×

This is the most dramatic row in the whole data set. Four controlled files, all four run through the same canvas path:

FileCharacterOriginalcanvas PNGChangeJPEG q92JPEG q80WebP q80
og-a.pngGraphic, 2,351 colors, RGB306,302607,481+98.3%56,45937,69513,852
quantized.png64-color palette59,843184,856+208.9%78,38844,44216,046
photo.pngPhoto, 479,373 colors498,639867,763+74.0%77,32145,05024,068
photo.jpgJPEG q8247,828809,415+1,592% (16.9×)58,762 (+22.9%)47,15223,862

That last row is 47,828 bytes in, 809,415 bytes out.

The mechanism explains a whole family of “compression made it bigger” reports. JPEG is a lossy frequency-domain codec: it transforms 8×8 blocks into DCT coefficients, quantizes them aggressively, and stores the survivors. PNG is a lossless spatial codec: it predicts each pixel from its neighbors and deflates the residuals. Decode a JPEG and you get pixels that carry every artifact the quantizer introduced: ringing near edges, blocking in smooth gradients, subtle noise where the original had none.

Save those pixels as PNG and you are asking a lossless codec to store the artifacts perfectly. It obliges. The very noise that JPEG created to make the file small is now the thing making the PNG large, because noise is exactly what a predictive lossless encoder cannot compress.

Anywhere a “save as PNG” default sits in front of a photo, this is happening: screenshot tools, design exports, chat clients, some upload widgets.

The same pixels, 3.46× apart, both lossless

The weakness of the canvas encoder is measurable. Take the pixel buffer from og-a.png and encode it two ways, both fully lossless:

EncoderOutputColor type
Chrome canvas toBlob('image/png')607,481 BRGBA (forced upsample)
upng-js encode(..., cnum=0)175,491 BRGB (original color type kept)

3.46×, for identical pixels. We verified the lossless claim rather than assuming it: decoding the upng-js output at cnum=0 returns a buffer that is byte-for-byte identical to the input RGBA buffer. Nothing was traded away for that 3.46×.

It also answers a question that comes up constantly: why do two online compressors, both free, both advertising the same thing, give results that are not remotely comparable? Because “compress in the browser” describes two completely different implementations. One hands the pixels to canvas.toBlob and ships whatever comes back. The other carries a real PNG encoder and controls the color type. Same input, same browser, 3.46× apart.

3. Cause two: your JPEG has nothing left to give

Five rounds of recompression, and the size stops moving

Take photo.jpg, which was already saved at quality 82, and recompress it at quality 0.8 five times in a row, each generation feeding the next:

GenerationBytesvs. previous
0 (original, q82)47,828
147,152−1.4%
247,164+0.0%
347,156−0.0%
447,1560.0%
547,1560.0%

The first pass buys you 1.4%. From generation 2 onward the file size is locked inside a ±12 byte band, and generations 4 and 5 are bit-identical in size to generation 3.

This is what “compression not working” looks like when the input is a JPEG. The tool ran. The encoder ran. There was nothing left to remove, because the quantization tables at quality 80 were already zeroing out roughly the coefficients that quality 82 had kept. Once a coefficient is gone it cannot be removed again.

You can watch it happen locally:

cp photo.jpg gen0.jpg
for i in 1 2 3 4 5; do
  magick "gen$((i-1)).jpg" -quality 80 "gen$i.jpg"
done
stat -f%z gen0.jpg gen1.jpg gen2.jpg gen3.jpg gen4.jpg gen5.jpg

On Linux use stat -c%s instead. The exact byte counts depend on which encoder your ImageMagick build links against, so do not expect the table above to reproduce digit for digit. The shape is the point: one meaningful drop, then a flat line.

Turning the quality number up is not compression

Look again at the photo.jpg row in section 2. Recompressing that quality-82 original at quality 92 produced 58,762 bytes: +22.9%.

This surprises people who assume the quality parameter is a dial from “small” to “large” that they are free to set anywhere. It is not an absolute quality target. It selects a quantization table, and running a decoded image back through a finer table than the one that produced it stores the existing artifacts more precisely while adding a fresh round of loss on top. Bigger file, worse picture, both at once.

The rule that falls out of this: never recompress a JPEG at a quality setting above the one it was saved with. If you do not know what that was, do not recompress at all. Go back to the source.

Generation loss: the JPEG recompression quality loss you cannot see

The plateau table has a trap in it. Size stopped changing after generation 2, but the image kept changing. Every pass decodes to pixels, re-transforms, and re-quantizes. Coefficients that survived on a boundary in one generation get pushed over it in the next.

The damage does not appear where you look for it. At thumbnail size, generation 5 and generation 1 are indistinguishable. Zoom to 100% and check the places JPEG always fails first: hard edges against flat backgrounds, text, and smooth gradients where blocking appears as visible 8×8 tiles. In a build pipeline that recompresses on every deploy, this accumulates silently for months.

We measured file sizes here, not perceptual quality, so there is no PSNR or SSIM figure for the five generations to quote: we did not measure one. The size data carries the point on its own. From generation 2 the cost is entirely on the quality side and the benefit is zero.

Color shifts are a separate failure with the same trigger. Canvas carries no metadata, so a toBlob round-trip drops the EXIF block and the ICC profile along with it. An image tagged Display P3 or Adobe RGB going in comes out untagged, which viewers will interpret as sRGB. The pixel values did not move. The instructions for interpreting them did.

4. Cause three: PNG file size not reducing because it was already quantized

If your PNG has already been through a compressor once, the second pass has nothing to work with. This is the quantized.png row, a 59,843-byte file already reduced to a 64-color palette, run down three separate paths:

PathResultvs. original
Lossless re-encode (upng cnum=0)61,377+2.6%
Quantize to 256 colors61,366+2.5%
Quantize to 64 colors61,366+2.5%

Every path came out larger than the original. Not by much, but larger, and that includes quantizing to 64 colors a file that already had 64 colors.

PNG compression works by removing redundancy: repeated colors, predictable neighbors, a small palette. A previous pass already collected all of it. What is left is close to incompressible, and the tiny increase is the encoder’s own overhead: a slightly different palette ordering, different filter choices per scanline, a marginally less lucky deflate.

The practical consequence is that “0% saved” on an already-optimized PNG is the correct result, not a failure. A tool that reports a small increase and then keeps your original file is behaving properly. A tool that hands you the larger file anyway is not.

5. Quantization is the lever for PNG, not re-encoding

Lossless re-encode versus 256 colors versus 64 colors

Three files, each run through three strategies:

FileOriginalLossless (cnum=0)256 colors64 colors
og-a.png306,302175,491 (−42.7%)110,772 (−63.8%)76,230 (−75.1%)
photo.png498,639587,863 (+17.9%)109,284 (−78.1%)61,397 (−87.7%)
quantized.png59,84361,377 (+2.6%)61,366 (+2.5%)61,366 (+2.5%)

Lossless re-encoding is the weakest tool available. It won 42.7% on the graphic, gave back 17.9% on the photo, and lost 2.6% on the already-quantized file. Photographic content defeats even a competent lossless PNG encoder, because there is no palette to find and neighboring pixels do not predict each other well.

Quantization is where the reduction comes from, and the gap is not close: 63.8% versus 42.7% on the same graphic at 256 colors, 75.1% at 64. On the command line:

magick logo.png -colors 64 PNG8:logo-64.png
magick logo.png -colors 256 PNG8:logo-256.png

The PNG8: prefix forces a palette PNG. Without it, ImageMagick may reduce the colors and then still write a truecolor file, which throws away most of the benefit.

When a palette is safe, and when you get banding

Quantization is lossy. It maps every pixel to the nearest entry in a limited palette, so the question is whether your content has enough distinct colors for that to be visible.

A palette is safe for icons, logos, UI screenshots, flat illustrations, diagrams, and anything else with large regions of uniform color and hard edges. Those typically contain a few hundred distinct colors at most, so a 256-entry palette is nearly free and even 64 often survives.

It gets risky with photographs, smooth gradients, soft drop shadows, and semi-transparent overlays. Reducing a gradient to 64 steps produces visible bands, and dithering trades those bands for noise that then costs you back some of the file size. Partial transparency over a gradient is the hardest case of all.

Transparency deserves its own check, because how much of it survives depends on the encoder rather than on quantization itself. ImageMagick’s PNG8: writes binary transparency, so a pixel is either fully opaque or fully clear and a soft anti-aliased edge comes back hard. A dedicated PNG quantizer keeps the full alpha channel and the soft edge with it. If your asset has a drop shadow or feathered edges, compare the two before you commit.

Check the result at 100%, not in a thumbnail. Banding is the one artifact that a scaled-down preview reliably hides.

6. The fix: change format instead of recompressing

A format decision table

ContentUseWhy
PhotographsWebP, or JPEG for maximum compatibilityLossy frequency coding is what photos need
Screenshots, UI graphicsQuantized PNG, or WebPFlat colors, hard edges, small palettes
Icons and logosSVG when you have the vector, otherwise quantized PNGVectors have no resolution problem
Anything needing transparencyWebP or PNGBoth carry a full alpha channel
AnimationWebPOne format instead of a GIF
Pixel-exact archivalPNG, losslessThe only case where lossless is the requirement

This is the short version, deliberately. Encoding efficiency and browser support across the modern formats have their own article: WebP vs AVIF vs JPEG.

What WebP did to the same 83 files

The 83-file set that grew 100% of the time through canvas PNG, converted instead to WebP at quality 0.8: 83 of 83 got smaller, median −94.2%. The single file from earlier, aes-decrypt.png, went from 505,516 B to 27,188 B, −94.6%.

The controlled files agree. og-a.png: 306,302 as PNG, 13,852 as WebP q80. photo.png: 498,639 as PNG, 24,068 as WebP q80. photo.jpg: 47,828 as JPEG, 23,862 as WebP q80.

One caveat on that last comparison. WebP at quality 0.8 is lossy, so it is not competing with PNG on equal terms, and re-encoding an existing JPEG into WebP still costs you a generation. Compare it against the file you were about to ship, not against a hypothetical perfect original.

When you should keep the PNG anyway

Lossless is sometimes the requirement, not a preference. Keep PNG for assets that go back into a design pipeline and get edited again, for screenshots used in pixel-comparison tests, for UI slices where a single shifted color breaks a visual diff, and for anything that will be composited later where quantization artifacts would compound. In those cases, run a proper quantizing encoder if the content allows it and accept the file size if it does not.

One more kind of “bigger” that has nothing to do with compression

If you inline images as data URIs, the size in your CSS or HTML is not the size on disk. Base64 encodes every 3 bytes as 4 characters, plus padding, so the text is arithmetically +33% larger than the bytes it carries, before any transfer compression. A perfectly optimized image still gets a third bigger the moment you inline it. When that tradeoff is worth making is covered in our guide to data URI inlining.

7. Doing it: browser, command line, build pipeline

In the browser

When you pick a browser-based tool, the question to ask is whether PNG goes through canvas. In our image compressor, it does not. PNG input is quantized to a color palette and written back out as a real PNG with its alpha channel intact, so transparency and soft edges survive; the quality slider maps to a palette size rather than to a toBlob argument that PNG would ignore anyway. Quality 100 maps to a lossless re-encode. JPEG and WebP do go through canvas, where the quality parameter is real and does what you expect.

One behavior looks like a bug and is not: if the compressed result is not smaller than the file you uploaded, the tool throws its own output away and keeps your original bytes. On an already-optimized PNG you will see “0% saved”. That is section 4 working as intended.

Your browser does the work, so the files never leave your machine.

On the command line

cwebp ships with libwebp and is the fastest way to test whether a format change solves your problem:

# Lossy WebP, quality 0-100
cwebp -q 80 photo.png -o photo.webp

# Lossless WebP, compression effort 0-9
cwebp -z 9 logo.png -o logo.webp

ImageMagick 7 covers the conversion and quantization cases:

# PNG to JPEG at a chosen quality
magick photo.png -quality 80 photo.jpg

# Quantize to a 64-color palette PNG
magick logo.png -colors 64 PNG8:logo-64.png

# Drop EXIF and other metadata
magick photo.jpg -strip photo-clean.jpg

On macOS, sips is already installed and needs no dependencies:

# Convert to JPEG; formatOptions takes 0-100 or low/normal/high/best
sips -s format jpeg -s formatOptions 80 photo.png --out photo.jpg

# Resize so the longest edge is 1200 px, aspect ratio preserved
sips -Z 1200 photo.jpg --out photo-1200.jpg

# Read the dimensions back
sips -g pixelWidth -g pixelHeight photo-1200.jpg

Resize before you compress. Encoders work on pixels, and the cheapest pixel is one that does not exist.

In a build pipeline

Once this is automated rather than manual, the decision moves to where the work happens and which library does it, which changes the tradeoffs entirely: browser-based versus Node.js image compression covers that comparison. The one rule that carries over from this article: compress from the original source file on every build, never from the previous build’s output. That is how a pipeline walks itself down the generation-loss curve while the file sizes look perfectly stable.

8. Five beliefs the measurements do not support

“Compressing twice makes it smaller.” Generation 1 bought 1.4%. Generations 2 through 5 stayed within ±12 bytes while the picture kept degrading. The second pass is pure cost.

“PNG is lossless, so it is the better format.” Lossless is a property, not a virtue. Our test photo is 498,639 bytes as PNG and 24,068 as WebP q80. Preserving a photograph bit-perfectly when it will only ever be viewed on a screen buys nothing and costs most of the file.

“Quality 100 is the safe choice.” Recompressing a quality-82 JPEG at quality 92 produced +22.9% and a worse image. Above the original’s quality setting, the number stops meaning “safer” and starts meaning “bigger”.

“The file is large because the resolution is high.” Resolution matters, but format matters more at the same resolution. og-a.png is 1200×630 either way: 607,481 bytes as a canvas PNG, 13,852 bytes as WebP q80. Identical pixel count.

“Online compressors are all the same.” Same pixels, same browser, both lossless: 607,481 bytes from canvas, 175,491 from upng-js. A 3.46× spread between two tools that describe themselves identically.

9. FAQ

Why is my compressed image bigger than the original?

A compressed image comes back bigger than the original when the tool re-encoded it instead of compressing it. Browser canvas output is always 32-bit RGBA PNG, which discards palettes and stores four bytes per pixel. In our test of 83 real PNGs, all 83 grew, with a median increase of +76.6%. Convert to WebP or JPEG instead.

Why does my PNG not get smaller when I compress it?

PNG is lossless, so a quality slider has nothing to trade away. Real reduction comes from cutting the color count, and if the file was already quantized there is nothing left to cut. Our 64-color test PNG came back at +2.5% after being quantized to 64 colors a second time.

Does compressing a JPEG twice lose quality?

Compressing a JPEG twice does lose quality, and you get almost nothing in return. Five rounds at quality 0.8: 47,828 bytes down to 47,152 on the first pass, then locked within 12 bytes for the next four. The size stopped moving while every pass kept re-quantizing the picture. Keep your originals.

Should I use PNG or JPEG to make a file smaller?

For photographs, JPEG or WebP, without exception. Our test photo measured 498,639 bytes as PNG, 45,050 as JPEG q80 and 24,068 as WebP q80. Reserve PNG for flat graphics, sharp text and transparency, where a small palette does the compression work.

Why did my image get blurry after compression?

An image gets blurry after compression for two different reasons. Lossy encoders at low quality settings produce visible blocking around edges and text. Repeated passes add generation loss even when the file size stops changing. If the colors shifted rather than softened, the canvas round-trip dropped your ICC profile, since canvas carries no metadata at all.

Can I compress an image without losing quality?

You can compress an image without losing quality, but expect far less reduction. Lossless re-encoding only rewrites identical pixels more efficiently: 306,302 to 175,491 bytes on our graphic, verified byte-for-byte identical after decoding. The same approach on a photo went the wrong way, +17.9%. For real savings with no visible loss, use WebP at quality 80.

Why is my PNG so large when it is only a screenshot?

Screen captures are stored as full RGBA PNG, four bytes per pixel before compression, and a Retina display doubles the pixel count in each dimension. Flat content responds well to palette reduction: our Open Graph card graphic dropped 63.8% at 256 colors and 75.1% at 64.

Does resizing reduce file size more than compressing?

Resizing usually reduces file size more than compressing, and the two compound. Halving both dimensions removes three quarters of the pixels before the encoder starts, and file size broadly follows pixel count. Resize to the dimensions the image is displayed at, then compress once. A full-resolution camera file dropped into a thumbnail slot wastes both passes.

Tags: image-compression png jpeg webp canvas debugging

Related Articles

View all articles