Responsive design, the currently trendy new way of getting your websites to work unaltered across a range of devices, has been getting a lot of attention lately. I’m a big fan of responsive design: I do a certain amount of web-browsing on an iPod Touch, and - for all Apple’s pinch-to-zoom wizardry, reading non-responsive pages can be painful.
Responsive design is pretty new, but its adherents have figured out a good set of basic techniques which work well in most cases. Unfortunately, in the world of the web, no problem stays solved for long. Just when everything looked rosy, Apple came along and messed everything up for everyone.
They did this, not in the way that Internet Explorer used to - by being too lame - but by being too good. The problem is Apple’s new high-resolution Retina display, as shipped in their latest piece of so-sexy-I-want-to-lick-it-all-over consumer electronics, the new iPad. The problem, basically, is this: the Retina display is so good that images sized for a standard resolution display look ugly. Now, the responsive folks want a way to serve up clean, crisp hi-res images to the iPad and its ilk, and boring, stodgy old lo-res images to those poor souls still suck on 72dpi displays.
My sometime colleague Kelly Sutton does a good job of summarizing the issues in a blog post about the future of retina images on the web. He also proposes a solution of his own. He - rightly, in my view - thinks that handling things in Javascript, using a library such as foresight.js, is probably not the way to go. This issue - getting the right content for your client - is basic enough that it shouldn’t be left to ad hoc Javascript. It should be up to the browser to figure out what it wants and how to ask for it it.
Apple, having unleashed their monster on the world, had to tackle the problem themselves on their own site. They did so by loading the standard images and then - using Javascript - probing the server to see if it had any hi-res resources available, and downloading those. This, as Kelly implies, is madness, not Sparta. It causes both the hi- and lo-res versions to be downloaded (wasting bandwidth); it adds in a redundant HEAD request for each image, and it depends on Javascript.
Kelly proposes that instead the browser itself should be capable of sending HEAD requests for the possible variants of each image. If the page includes an ‘image.png’, and the browser is running on a device with a Retina display, it should send a HEAD request for both ‘image.png’ and for ‘image-2x.png’. Depending on what it gets back, it should then decide which image to load into the page. In this way, you avoid wasting bandwidth by downloading a lo-res version that you’re only going to throw away.
There are some problems, however. First, the number of requests is climbing: for each image, you’re sending two - or more - HEAD requests, followed by a GET once you’ve decided which image you’re actually going to use. In these days of Keep-Alive connections, that may not be such a big deal, but it seems inelegant. If nothing else, the server’s error log files are going to explode with 404’s. If the site’s ‘not found’ handling involves any significant processing, the server will also take a noticeable hit.
Then there’s the question of layout. Some browsers will hold off on laying out the page until they know how big an image is going to be. Yes, we should all be specifying our image sizes in the <img> tag or, better, in the CSS. Not everyone does, though. So the browser either has to wait until all the HEAD requests and the final GET request have returned, or it has to start laying things out and then do it all over again (producing an ugly page redraw) once the final results are in. They have to do this now anyway, but with more requests in the pipe, the wait is longer.
How can we avoid this awkward fumbling with HEAD requests, with the user agent essentially feeling the web server up to see what it’s got? My suggestion would be: don’t play a guessing game; if you want something, come straight out and ask for it.
The HTTP-Accept header is one of the lesser-used features of the HTTP standard. It allows a browser to say “I can handle these particular formats, and here’s my order of preference”. In theory, the web server could inspect the list and return the most appropriate content. In practice, almost no servers do anything useful with the HTTP-Accept header. Many do, however, do something very useful with the Accept-Encoding header: if a browser uses Accept-Encoding to say that it can accept compressed content, the server can give back the web page in compressed form, for substantial savings all round.
In principle, HTTP-Accept and Accept-Encoding represent, for me, a better way to approach this problem. The browser says explicitly what it would like; the server, if it can, honors the browser’s preferences. There are no extra HEAD requests, no waiting: the browser’s desires are expressed in the request, and the server comes straight back with the content that it wants.
So a browser could send, for example, a header called Accept-Resolution, with values such as ‘low’, ‘standard’, ‘high’, depending on the resolution of its screen (and the speed of its connection). The server then hands back the appropriate version of the image.
You could probably implement this today, using mod_rewrite rules. You could even - if you don’t mind browser-sniffing - do it on the server side without waiting for someone to implement an Accept-Resolution header.
There’s one big gotcha, and that’s caching proxies. If Joe Desktop requests a standard image for their PC and then Jane iPad uses the same proxy, Jane is going to see the jaggies on her Retina display. If Jane gets there first, Joe gets served up a gigantic image he can’t use (and if Joe Desktop is actually Joe Smartphone on a slow 3G connection, that’s going to hurt).
So either proxies will need to be updated to handle the ‘Accept-Resolution’ header, or images will have to be served with no-cache directives. I’d like to rule out the second ‘solution’ for reasons of economy, and suggest that proxies will need to be made resolution-aware.
Are there other options? I can think of just one more. Instead of having the browser say what it would like, the server can tell it what it can have, using a response header. When the agent requests the initial web page, the server returns a header that says, in effect, ‘hi-res spoken here’. That response would be an invitation to the browser to dynamically rewrite each image request to specify a hi-res version instead of the base version (we can use Kelly’s proposed naming conventions). Before the agent even knows which images it’s going to need to download, the server has already told it what it’s allowed to ask for. If there’s no hi-res version available for any particular image, the server just returns a standard-res version. Finally, as a not-insignificant bonus, the different resolution versions have different names, so we get around the whole caching problem.
I agree with Kelly that this is something that belongs in the browser, not in Javascript, but I’m convinced that the long-term solution needs to use headers, not HEAD requests.