You need 2x image files whenever an image is shown at a fixed CSS size and has to look sharp on Retina and other HiDPI screens, which pack two or more physical pixels into every CSS pixel. For most sites that means serving a 1x and a 2x version with srcset, using SVG for logos and icons, and not bothering much with 3x.
What is device pixel ratio?
Device pixel ratio (DPR) is the number of physical screen pixels used to draw one CSS pixel. MDN describes window.devicePixelRatio as the ratio of the display's physical pixels to CSS pixels. You can check yours by typing devicePixelRatio into the browser console.
A few typical values:
- Most older desktop monitors: 1.
- MacBooks and most iPhones: 2 or 3.
- Android phones: all over the place, including 1.5, 2.625, 2.75, and 3.
- Windows laptops with display scaling at 125% or 150%: 1.25 or 1.5.
DPR can also change on the same device. Zoom the page in and the ratio goes up. Drag a window from a laptop screen to an external monitor and it can drop back to 1. Browsers take this into account when they pick an image from srcset, though they don't always re-download when it changes mid-visit.
Why do 1x images look blurry on Retina screens?
Say your logo sits in a 200×50 CSS pixel box. On a 2x screen, that box covers 400×100 physical pixels. If the file is only 200×50, the browser has to invent the missing pixels by scaling up. Scaling blurs edges.
Photos hide this fairly well. Flat graphics don't. Crisp type, thin strokes, and hard color edges in a logo turn fuzzy fast, and that's usually where people first notice the problem. A blurry logo next to razor-sharp system text looks cheap, even to people who couldn't tell you why.
How do you serve 2x images to fixed-size elements?
For anything shown at one fixed CSS size, like a logo, an avatar, or a small badge, use srcset with x descriptors. You tell the browser which file suits which density, and it picks.
<img src="/img/logo-200.png"
srcset="/img/logo-200.png 1x, /img/logo-400.png 2x"
width="200" height="50"
alt="Acme Hardware">
The steps:
- Decide the display size in CSS pixels (here, 200×50).
- Export the 1x file at that size and the 2x file at double (400×100).
- List both in
srcsetwith1xand2x. - Keep a plain
srcas the fallback. Point it at the 1x or the 2x file, whichever you'd rather old clients get. - Set
widthandheightto the CSS size so the browser reserves space. More on that in our post on image width, height, and layout shift.
Don't forget the width and height. Without them, a browser that loads the 2x file may show it at its natural 400px width, which is double the size you wanted.
What about images that change size with the layout?
A hero image that fills the column on a phone and sits at 800px on a desktop doesn't have one CSS size. For these, x descriptors are the wrong tool. Use w descriptors and a sizes attribute instead.
<img src="/img/hero-800.jpg"
srcset="/img/hero-400.jpg 400w,
/img/hero-800.jpg 800w,
/img/hero-1200.jpg 1200w,
/img/hero-1600.jpg 1600w"
sizes="(max-width: 840px) 100vw, 800px"
width="1600" height="900"
alt="Workbench with hand tools laid out">
Here the w number is the real pixel width of each file. sizes tells the browser how wide the image will display. The browser multiplies that display width by the screen's DPR and grabs the closest file. So a 390px-wide phone at DPR 3 wants roughly 1170 pixels and will likely pick the 1200w file. You don't write any density logic yourself.
This is the part most people get wrong, so we wrote a whole post on responsive images with srcset and sizes. The short version: include a large enough top file to cover 2x at your biggest layout width.
Do you need 3x images?
Usually not. The jump from 1x to 2x is easy to see. The jump from 2x to 3x is much harder to spot at normal viewing distance, and a 3x file has 2.25 times the pixels of a 2x file. That's a lot of bytes for a small gain.
Jake Archibald, in his write-up on sharp images for dense screens, makes a similar point: you can go further for higher densities, but the benefit isn't as big as going from 1x to 2x, so it isn't always worth it. Many large sites cap their image selection at 2x for this reason. Some image CDNs and frameworks let you set a max DPR for the same purpose.
There are fair exceptions. Product zoom shots, photography portfolios, and small UI images with fine text can justify 3x. Decide per image type, not site-wide.
With w descriptors, you can't directly tell the browser "stop at 2x." What you can do is not generate files larger than your 2x need. If the biggest file in srcset is 1600w and the image displays at 800px, a 3x screen just gets the 1600w file. That's a soft cap, and it works.
Should logos and icons be SVG instead?
Yes, if the artwork is vector. An SVG is drawn from shapes, not pixels, so it's sharp at 1x, 2x, 3x, and whatever comes next, all from one file. A simple logo SVG is often smaller than a single PNG of it.
SVG is a poor fit for photos or anything with detailed textures. Those stay raster. For the details on embedding methods, accessibility, and upload safety, see our guide to using SVG images on websites safely.
Favicons are a special case. Browsers ask for several fixed sizes, and they don't all accept SVG. Our favicon sizes guide covers which PNG sizes to ship for HiDPI tabs and home screens, and which ones an SVG icon can replace.
How do you serve 2x images in CSS backgrounds?
Use image-set(). It works like srcset with x descriptors, but in CSS:
.banner {
background-image: url("/img/pattern-1x.png");
background-image: image-set(
url("/img/pattern-1x.png") 1x,
url("/img/pattern-2x.png") 2x
);
}
MDN lists image-set() as Baseline and widely available since September 2023. The first plain url() line is a fallback for older browsers that don't understand the function. It's cheap to keep.
image-set() can also pick by format with type(), so you can offer AVIF with a JPEG fallback. That's handy, though mixing density and format options in one list gets messy quickly. Our WebP vs AVIF vs JPEG vs PNG comparison can help you decide whether that's even worth it for a background.
The older approach, a @media (min-resolution: 2dppx) block that swaps the URL, still works. image-set() is shorter and lets the browser make the call.
Can you compress 2x images harder?
Often, yes. This is sometimes called the "compressive images" technique. The idea is that on a dense screen, each compression artifact is physically tiny, so your eye doesn't pick it up the way it would at 1x.
web.dev's article on high DPI images shows a test where a heavily compressed 2x JPEG looked better than an uncompressed 1x one. Jake Archibald's example is more concrete: a 2x image at quality 80 was 45.2 kB, while the same 2x image at quality 44 was 21.2 kB and still looked sharp on a 2x screen. The 1x at quality 80 was 14.9 kB. So the lower-quality 2x cost only a few kB more than the 1x and looked much crisper.
Some caveats, because this isn't free:
- It works best on photos. Logos and screenshots with flat color show artifacts sooner.
- If you serve one heavily compressed 2x file to everyone, 1x screens have to download and scale down a larger image. web.dev warns about the extra memory and decoding cost on low-end devices.
- The right quality number depends on the image and the encoder. Don't copy "44" and walk away. Compare a few settings side by side on a real 2x screen.
Our post on image compression quality settings goes through how to test this without guessing.
Common mistakes
- Shipping only a huge 2x file with no
srcset. 1x screens pay for pixels they can't show. - Using
xdescriptors on a fluid image. If the image width depends on the viewport,xcan't know how big it'll be. Usewandsizes. - Leaving out
sizeswithwdescriptors. The browser then assumes the image spans the full viewport (100vw) and may fetch a much bigger file than needed. - Upscaling a 1x source to make a "2x" file. That adds bytes and no detail. Export from the original artwork or a larger photo.
- Exporting a vector logo as PNG. Use SVG and skip the density question entirely.
- Lazy-loading the hero image. Density isn't the only thing that slows your largest image. See lazy loading and LCP for why the main image should load eagerly.
Icons feel the density problem most. A 16-pixel favicon is 32 physical pixels on a 2x screen, so a blurry source shows fast. Start from a large, clean mark and let the favicon converter produce every size, including the 180×180 Apple touch icon.
TL;DR
You need 2x files for any raster image that has to look sharp on HiDPI screens. Use x descriptors for fixed-size images and w plus sizes for fluid ones, use SVG for logos and icons, and use image-set() for CSS backgrounds. Stop at 2x unless an image really needs more, and test whether you can compress the 2x version harder.
