Someone in a forum thread says use 80. You set 80. The photograph on the homepage loses two thirds of its weight and nobody notices a thing. The screenshot in your documentation comes back with grey fuzz around every letter, a faint halo down the edge of the sidebar, and a red heading that has gone slightly muddy. Same encoder, same number, two completely different outcomes.
The number is not a percentage. Nothing in the file is eighty percent of anything. In JPEG it is an index into a formula that scales a quantisation table. In WebP it drives a quantiser and a set of encoder decisions that have no relationship to the JPEG one. WordPress core is quietly explicit about this: WP_Image_Editor::get_default_quality() returns 82 for image/jpeg and 86 for image/webp. Two different numbers, because the same number would not have produced comparable output.
So the useful question is not which quality setting is best. It is this: for this image, at the size it will actually be displayed, where does the file stop getting meaningfully smaller and start getting visibly worse? That has an answer, and finding it takes about fifteen seconds.
What lossy compression discards first
JPEG splits the image into one luma channel and two chroma channels, usually throws away three quarters of the chroma resolution outright, then cuts the luma into 8 by 8 blocks and runs a discrete cosine transform on each block. Sixty four pixels become sixty four coefficients, ordered from flat to fine. Each coefficient is divided by an entry in a quantisation table and rounded to a whole number. The fine detail entries are the largest divisors, so fine detail is what rounds away to zero first.
On a photograph, fine detail is leaf texture, skin pores, sand grain and sensor noise. Deleting a good part of it changes nothing you could put into words. On a screenshot, fine detail is the content: a one pixel border, the stems of the letters, the hard step between two flat fills. A cosine transform has no compact way to describe a step edge, so what comes back is ringing beside it, and the discarded chroma turns coloured text into a smear. That is the whole reason one recommended number cannot serve both files.
The way out is to stop arguing about the number and measure it. Drop a file into the image quality comparison tool below and it encodes your image across a sweep of quality values in JPEG and WebP, decodes every result again, and scores each one against the original. All of it runs in your browser: nothing is uploaded, and the page makes no network request with your image in it.
Image quality curve
Drop one image and get a measured answer to the question the quality slider never answers: which number is right for this image. It is encoded across a sweep of quality values in JPEG and WebP, every result is decoded again, and each decoded result is compared with the original by SSIM over the luma channel, computed here in the tab. SSIM is a structural similarity measure, not a perceptual model like Butteraugli, and it does not know what your eye forgives. Nothing is uploaded: every byte is written and read inside this browser tab.
No image yet. The two sample images are drawn in this tab, not downloaded, and they behave very differently under a quality sweep.
Eight quality values from 30 to 100 in steps of 10, in two formats.
These settings have changed since the last measurement. The numbers below are still the old ones until you press Measure again.
Nothing measured yet. Drop an image above, or press Try a sample photo to watch the curve appear.
Solid lines are bytes on the left axis, dashed lines are SSIM on the right. The ring marks the knee, the last point where more bytes still buy a real gain in similarity. SSIM compares local means, variances and covariance of the luma channel through a sliding window. It is a structural measure, not Butteraugli and not an eye: a file can sit at 0.99 and still show a visible ring around a hard edge, which is what the difference view underneath is for.
Move the pointer over either image, or focus one and use the arrow keys. The magnifier shows both panels at 1:1, one screen pixel per image pixel, original on the left.
The difference view amplifies the per channel error so that it is visible at all: at the amplification above, a difference of one step out of 255 is drawn as a difference of twelve. Where the error lands is the point. On a photograph it hides in texture, on a screenshot it collects around type and hard edges, which is why one number for both is a bad habit.
| Format | Quality | Bytes | Saving | SSIM | What the last step cost | Verdict |
|---|
Deliberate limits, and they matter. SSIM is structural similarity on the luma channel: 1.0 means identical, and it says nothing about a colour shift, banding in a flat sky, or how the picture holds up on a phone in the sun. It is not Butteraugli, not SSIMULACRA, not a perceptual model. The second limit is the encoder: this is the browser's JPEG and WebP writer, not your server's. mozjpeg, GD and ImageMagick all write different bytes at the same number, and WebP here is a single pass with the browser's own settings. So read the shape of the curve and the position of the knee, which do transfer, and treat the absolute byte counts as indicative only. An animated GIF is measured on its first frame. Colour profiles and EXIF are dropped by the canvas, a server keeps some of them.
The quality scale is a knob, not a measurement
Inside libjpeg, the number you type is converted to a scaling factor by two lines of arithmetic. At 50 and above, the factor is 200 - 2q. Below 50, it is 5000 / q. Every entry in the standard quantisation table is then multiplied by that factor, divided by a hundred, rounded, and clamped to a minimum of 1. Quality 50 leaves the table exactly as the standard defines it. Quality 75 halves it. Quality 90 divides it by five, and quality 100 flattens every divisor to 1.
Two things follow that surprise people. The scale is violently non-linear: moving from 50 to 55 changes the factor from 100 to 90, while moving from 90 to 95 halves it from 20 to 10. And quality 100 is still lossy, because chroma subsampling and the rounding in the transform itself do not go away when the divisors reach 1. There is no setting on the JPEG slider that gives you your pixels back.
WebP’s number is a different knob on a different machine. It sets a quantiser, and it also moves the filter strength, the number of analysis passes and the segmentation the encoder is willing to do. Chromium’s canvas writer goes further and switches to lossless mode at quality 100, which is why a WebP written at 100 can be a smaller file than the same image at 90 when the artwork is flat. The sweep in the tool runs its recommendation over the Pareto frontier rather than the raw list for exactly this reason, and when that inversion happens it says so: the higher number is also the smaller file.
One consequence worth stating plainly. The encoder in your browser is not the encoder on your server. Chromium’s JPEG writer is not mozjpeg, and its WebP writer is not cwebp at method 6. The absolute byte counts you measure here will not match what GD or Imagick writes during an upload. The shape of the curve and the position of the knee do transfer, because both are properties of the image, not of the writer.
Why a photograph and a screenshot disagree
Run the same sweep on a landscape photograph and on a screenshot of an admin screen and the two curves do not even have the same shape. The photograph gives up bytes cheaply for a long stretch, because most of what it loses is noise it never needed. Its failure mode at the bottom is soft and gradual: texture goes waxy, skies start to band, then blocks appear.
The screenshot fails differently and much earlier. Loss concentrates exactly where you are looking, along type and hard edges, and it is visible as a defect rather than as softness. Switch the tool’s right panel to the difference view and this is obvious in a second: on the photograph the error is spread through the grass and the foliage, on the screenshot it traces every glyph and every border like a highlighter. Flat artwork with text usually has no useful lossy setting at all, and the right move is to keep it lossless. Which format that means is a separate question, covered in the guide to picking a format for WordPress, and this article does not repeat it.
The awkward cases are the mixed ones: a product photographed on a pure white background, a chart with a photograph inside it, a screenshot with a gradient behind it. Those have a photograph’s tolerance in one region and a screenshot’s intolerance in another, and the single averaged number hides the disagreement. This is the case where actually looking at the difference view matters more than reading the score.
What SSIM measures
SSIM does not compare pixels to pixels. It slides a small window across both images and, at every position, compares three statistics of the two patches: their means, which is local brightness, their standard deviations, which is local contrast, and their covariance, which is the extent to which they vary together. That third term is the structural one and it carries most of the weight. Two stabilising constants derived from the 0 to 255 range, 6.5025 and 58.5225, keep the ratios from exploding where a patch is nearly flat. Each window produces a number that reaches 1 only when the patches are identical, and the score for the whole image is the average over all windows.
The reason this beats a plain error measure is that it cares how the error is arranged. Scatter a given amount of error randomly through a photograph and SSIM barely moves, because the local statistics survive. Concentrate the same amount of error into a ring along one edge and SSIM drops hard, because the covariance in those windows collapses. That is a good match for what a reader notices. The tool offers the original paper’s window, 11 by 11 with a Gaussian weighting at sigma 1.5, and a faster 8 by 8 uniform one that is slightly harsher on edges. Both are averaged over the valid interior only, so no invented border pixels contaminate the result.
Now the limits, because a number with unstated limits is worse than no number. This implementation works on the Rec. 601 luma channel, weighting red, green and blue at 0.299, 0.587 and 0.114. It is therefore close to blind to a pure colour shift: desaturate every red in the picture and luma hardly moves, so SSIM will report that almost nothing happened. It is not a perceptual model in the sense that Butteraugli or SSIMULACRA are. And the scores are not comparable between images. A 0.98 on a noisy photograph and a 0.98 on a screenshot are not the same experience, so compare points within one curve and never across two files.
Reading the curve, and finding the knee
Plot bytes on one axis and SSIM on the other against quality and the picture is always the same in outline. Coming down from 100, the file size falls off a cliff while the similarity line stays nearly flat. Then, at some point that depends entirely on the image, the two trade places: similarity starts dropping steeply and the file size barely moves. The bend where that happens is the knee, and it is the answer you came for. Above it you are paying bytes for detail nobody perceives. Below it you are destroying structure and saving very little.
The table under the chart makes this concrete with a column most quality comparisons leave out: what the last step cost, in bytes saved per unit of SSIM given up. Read down that column and the knee announces itself, because the figure falls off a cliff the moment you pass it. For ordinary photographs at web sizes the bend usually sits somewhere in the seventies or low eighties for JPEG and a little lower for WebP, which holds structure better at low bit rates. For flat graphics it often does not exist: similarity is already sliding at 90, which is the curve’s way of telling you the file wants a lossless format.
If you have a house rule, set it and let it override the geometry. The floor control (“never go below 0.95, 0.97, 0.98 or 0.99”) always beats the knee when the two disagree, and if nothing in the sweep reaches the floor you asked for, the tool tells you that instead of quietly returning the best of a bad set. That failure is information: it means the image cannot survive lossy encoding at the size you are measuring.
One file, two answers
The same photograph does not have one correct quality setting, because it is not displayed at one size. This is what the “measure at” control is for. Encode at a long edge of 1440 and every artefact lands at full size in front of the reader. Encode the same source at 512 and half of the damage disappears into the downsample before anyone sees it, so the knee moves down and a lower setting becomes defensible. A hero image and a thumbnail cut from the same original genuinely want different numbers.
WordPress makes that concrete, because a single upload becomes a whole set of files at different dimensions, as the breakdown of WordPress image sizes lays out. Every subsize is its own encode, and by default they all get the same quality number. Since WordPress 6.8 the wp_editor_set_quality filter receives the dimensions of the size being written, so you can finally split the difference instead of picking one compromise for everything.
add_filter( 'wp_editor_set_quality', function ( $quality, $mime_type, $size ) {
if ( 'image/webp' !== $mime_type ) {
return $quality; // leave JPEG at core's 82
}
$width = isset( $size['width'] ) ? (int) $size['width'] : 0;
// Large renditions carry the visual weight, small ones do not.
return $width >= 1200 ? 84 : 76;
}, 10, 3 );
Numbers like 84 and 76 in that snippet are placeholders until you have measured your own library, which is the point of the whole exercise. Note also that the two ends of the range matter for different reasons: the large rendition is usually the Largest Contentful Paint element and its byte count is a direct page speed problem, while forty thumbnails on an archive page are mostly a request count problem where a few kilobytes each is not where your time should go.
Where the measurement stops being valid
Several things are deliberately outside the scope, and knowing them keeps you from over trusting a number. There is no batch mode and no server side encoder: one image at a time, and no mozjpeg, no cwebp tuning, no encoder effort setting. If your browser cannot write one of the two formats, that is detected from the data URL the canvas hands back and the format is dropped with a note rather than counted as a PNG in disguise. Two copies of the tool on one page are not supported.
The canvas also strips things on the way in. Colour profiles and EXIF do not survive it, so an image tagged Display P3 is measured after conversion and the tool cannot warn you about the colour shift that causes, which is a separate failure with a separate fix for washed out images. An animated GIF is measured on its first frame only. An image smaller than the SSIM window has no valid interior to average, so the tool falls back to every pixel and reports a margin of zero rather than pretending the usual guarantee still holds.
One detail in the arithmetic is worth understanding, because it changes what the saving column means. When the image was not scaled for measurement, saving is calculated against your original file, which is the comparison you want. When it was scaled, comparing a 1024 pixel encode against a 4000 pixel original would flatter every result, so the reference becomes a measured lossless PNG of the working image instead. The percentages then describe what the encoder achieved, not what the resize achieved.
From one measurement to a setting you can keep
Measure three or four images that genuinely represent your library rather than the one you happen to have open: a busy photograph, a clean one with large flat areas like a sky or a studio background, a screenshot, and whatever your most common product or hero shot looks like. You will get four different knees, and the spread between them tells you more than any single result. Set your default from the photographs, because they are the bulk of the bytes, and route the flat artwork away from lossy encoding entirely instead of trying to find a number that covers it.
Then apply it once, in the place where uploads pass through, rather than per file by hand. A filter like the one above sets it for everything WordPress writes from now on, and existing files need a pass with a batch converter such as the image processor to bring them in line. What you should not do is re-encode an already lossy file at a higher quality number, which adds bytes and repairs nothing: the detail was discarded the first time, and generation loss only accumulates.
The reason every recommended quality setting is wrong is not that the people recommending them are careless. It is that the number describes the encoder, not the picture, and the picture is the half that decides. Once you have watched the curve bend for your own files, the recommendation stops being a number someone gave you and becomes a decision you can defend, which is the difference between a setting you keep and one you change again next month.