The more I use my new iPad, the more I notice non-retina graphics. We’re spoiled by the many native apps with retina graphics, that when we browse the web, it feels like a second class citizen. It doesn’t have to. This problem isn’t going away, either. I bet we’ll start seeing retina displays in MacBooks pretty soon, and maybe even glorious 27″ retina cinema displays, too. But, even with just the iPhone 4, 4S and new iPad, we’ve got a lot of devices receiving subpar content. Let’s fix that.
The existing solutions suck.
There’s basically two popular solutions:
- Swap out images with retina versions with javascript. This is what Apple does. It requires that every retina device not only download a large retina graphic, but also the original, non-retina graphic, and do it in three requests (a GET, a HEAD, and another GET). That’s not a solution.
- Use media queries to set a retina graphic on the background of some element. This is a good solution. It only requires one request and it only targets retina devices. Unfortunately, this doesn’t solve the issue for img tags. This also brings us to the question: Should developers even have knowledge of retina graphics? Or should they just be used when they’re needed, and that’s it? I haven’t done any iOS development, but if I understand correctly, that’s how it’s handled in iOS. If the retina graphic exists, it’s used, otherwise it’s not. Simple. That’s how it should be.
So, seeing no good solution, I stopped looking, until today, when I was browsing the web on my iPad, coming across site after site serving me terrible, blurry, non-retina graphics, and I decided to solve it.
The Solution
My solution is to figure out which image to serve server-side. This means our code doesn’t specifically request a retina graphic (either in CSS with a media query, or in JS when it’s swapping them out). This doesn’t come without its own pitfalls, which I’ll discuss in a bit, but I’d say this is the best solution that we have today.
The idea is to set a cookie with the devicePixelRatio early on (in the head), then serve retina graphics to those clients. Here’s the JS:
<script type="text/javascript">// <![CDATA[ if(window.devicePixelRatio !== undefined) document.cookie = 'devicePixelRatio = ' + window.devicePixelRatio; // ]]></script>
And here’s my nginx config for our WordPress site:
location ~* ^(/wp-content/themes/room118)/img/(.+)$ { set $retina_uri $1/img-2x/$2; if ($http_cookie !~ "devicePixelRatio=2") { break; } try_files $retina_uri $uri $uri/ =404; }
This is basically looking for requests for files in our theme’s img directory, and trying to serve the same image in the img-2x directory. If it’s not there, no problem, fallback on the original image. This allows us to build an independent directory tree of just retina graphics. I think this is a better approach than how it’s done on iOS, which is by appending some special identifier to the filename (iOS uses filename@2x.ext). You could easily modify that regex and organize your graphics however you’d like and to use whatever path you’d like.
Caveats
- You need to set a width/height on all img tags. You should be doing this anyway.
- You need to use the background-size CSS property for background images.
Future Proof
While devicePixelRatio’s greater than 2 aren’t in consumer electronics just yet, they probably will be at some point in the future. We could easily modify this to serve images from an img-?x directory, or if you prefer filename identifiers, you could use @?x or so. So, with a little tweaking, this should work forever.
But this isn’t perfect.
We need JavaScript, we need cookies, and we need that JS snippet to run on every page the user might visit. There are ways around this:
- We could use a media query to have retina devices make a request (by setting a background image on something), catching that request server-side, and setting the cookie there. This is sort of hackish, and would probably happen after at least some images are requested. I’d stick to the JS solution.
- We could do some user agent sniffing, but we’d then have to blindly serve retina graphics to all iPads/iPhones/iPods. This isn’t a terrible idea, and is a reasonable fallback if for some reason the JS/cookie solution doesn’t work for you.
Moving Forward
I think we need a header. If all browsers on retina devices sent a special header, saying “hey, serve me retina graphics”, we could handle this entirely server-side. Here’s why this would be great:
- This could be a browser setting, and toggled by the user.
- This could be automatically toggled when the device has a slow Internet connection.
- This problem only exists on newer devices, meaning we don’t need to deal with legacy devices. Apple could solve this today, update mobile Safari, and we could confidently rely on the header, knowing that we’re not leaving (m)any devices out. Any iPhone 4(s)/new iPad users who don’t update will receive non-retina graphics. Oh well.
So, there it is. It’s in use on this site right now (just for our logo, until I can round up some higher resolution versions of the other graphics we’re using around the site). Check it out. We have an awesome responsive design that looks great on the iPhone and iPad in both portrait and landscape orientations. If you’ve got a better solution, feel free to leave it in the comments.