You upload a crisp photograph, it looks perfect on your laptop, and then somebody opens the site on a modern phone and the picture is faintly soft. Not broken, not obviously wrong, just slightly out of focus in a way you cannot quite point at.
Nothing is wrong with the file. The screen is simply drawing more dots than the image has pixels, and the browser is making them up.
Two different things called a pixel
The confusion at the heart of this is that the word pixel means two different things.
A device pixel is a physical dot on the screen. A CSS pixel is a unit of layout, roughly the size a dot used to be on an old monitor, kept as a reference so that a 300 pixel wide box stays about the same physical size everywhere.
On an old display these matched. On a modern phone one CSS pixel is drawn with two or three device pixels in each direction. The ratio is the device pixel ratio, and it is why a photograph placed in a 400 CSS pixel wide slot may actually need to fill 800 or 1200 real dots.
Supply 400 pixels of image data and the browser has to invent the rest by interpolation. Interpolation is a polite word for guessing, and guessing looks soft.
The fix, and why it is not simply “upload bigger”
The obvious response is to upload everything at three times the size. That fixes sharpness and breaks something else: the phone with the dense screen is usually the device on the slowest connection, and you have just tripled what it downloads.
What you actually want is different files for different situations, with the browser choosing. That is what the srcset attribute is for, and WordPress has generated it automatically for years, which is why most sites get this roughly right without anyone doing anything.
Roughly right, not right. It is worth knowing where it falls down.
What srcset and sizes actually say
Two attributes, doing two different jobs, and mixing them up is the usual cause of trouble.
srcset is a menu: here are the versions of this image that exist, and here is how wide each one is in real pixels. It is a statement of fact about your files.
sizes is a promise: here is how wide this image will be displayed, at various viewport widths. It is a statement about your layout.
The browser reads the promise, multiplies by the device pixel ratio, and picks the smallest entry from the menu that meets or exceeds the result. Crucially it does this before the stylesheet has necessarily been applied, which is why it has to be told rather than measuring.
And that is where it goes wrong: WordPress cannot know your layout, so it guesses a default like “this image will be as wide as the viewport”. If your image actually sits in a 700 pixel column, every visitor downloads a file far larger than needed. The information is not wrong, it is simply a promise nobody kept.
Registered sizes, and the ceiling nobody notices
WordPress creates copies of every upload at a set of registered sizes. Those copies are what fills the menu. If the largest registered size is 1024 pixels wide, then no matter how large your original is, a hero image spanning a wide desktop screen at a device pixel ratio of two has nothing suitable to choose and falls back to something too small.
Two things follow. First, check what sizes your theme actually registers, because the answer is often surprising. Second, if you have a full width hero, make sure something exists at 2000 pixels or more, and make sure it is well compressed, because it is a big file that many visitors will download.
There is a matching trap at the other end. WordPress skips generating a size if the original is smaller than it, so uploading a 900 pixel image where the layout needs 1600 produces no error and no large version. It just looks slightly soft forever.
Different images, not just different sizes
Everything above assumes the same picture at different resolutions. Sometimes that is not what you want.
A wide landscape photograph that works beautifully as a desktop banner becomes a thin strip on a phone, with the subject reduced to nothing. The answer is not a smaller version of the same crop, it is a different crop: a squarer or portrait version where the subject still fills the frame. That is art direction, and it needs the picture element rather than srcset, because you are switching images rather than resolutions.
It is more work, so save it for the images that carry weight: the hero, the product shot, anything where the composition is the point.
Where the soft look really comes from
Resolution is only half the story. Three other things soften an image, and they are easier to fix.
Resizing without sharpening. Every downscale loses fine detail; that is what downscaling is. Professional workflows always finish with a small amount of sharpening after the resize, tuned to the final size. Skip it and the image looks slightly veiled even at the correct dimensions.
Compression at the wrong point. Fine detail is exactly what lossy compression discards first. An image at quality 60 will look soft at any resolution.
CSS scaling. An image sitting in a container that is not an exact multiple of its natural size gets resampled by the browser, which is fast rather than good. Odd fractional widths are a common cause.
A practical checklist
- Work out the widest a given image will ever be displayed, in CSS pixels.
- Double it. That is the largest version you need; going to three times is rarely worth the bytes.
- Make sure a registered size exists at that width, and that your originals are at least that large.
- Correct the
sizesattribute for images that sit in a fixed column, rather than accepting the default. - Sharpen after resizing, gently, at the final size.
- Check at exactly 100 per cent zoom, on an actual dense screen if you have one.
The one line worth remembering
Supply about twice the pixels the layout asks for, compress them properly, sharpen after resizing, and tell the browser the truth about how wide the image will be. Everything else is detail.