5 April 2012

Conditionally serving high resolution images

Introduction

The transition to high resolution displays – Retina or HiDPI displays in Apple's marketing terms – is happening right now. The iPhone got it in 2010, the iPad in March 2012, and Macs are around the corner. Update: The MacBook Pro with Retina Display was introduced on 11 June 2013. Rather than introducing resolution independence, Apple changes the ratio between logical pixel and device pixel. For each logical pixel, four physical pixels are displayed.

iOS uses a simple naming convention to decide between high resolution and standard resolution images within apps. If a high resolution display is available, iOS first looks for images that have the postfix @2x and then uses the normal filename as a fallback. This is convenient for developers, as no additional code is required to support both resolutions.

On the web, this approach is not feasible. Requests are expensive, so it is not possible to first try to retrieve the high resolution image and then send a second request if the first fails. The correct decision about which image URI to request needs to be made by the client without sending two requests.

Fortunately, WebKit supports the device-pixel-ratio media query in CSS. This makes it straightforward to serve high resolution background images only to devices that support them. The stylesheet needs to explicitly specify which images are available in high resolution.

For content images (img tags), this solution is not available, as there is a single src attribute for all devices. Workarounds exist that rely on additional markup and JavaScript. The additional markup consists of an attribute that contains a secondary URI for a high resolution image. A script iterates through all images and switches to the secondary URI on high resolution devices.

Alternatively, one could simply serve high resolution images regardless of display type. However, this means that a lot of bandwidth is wasted for devices that cannot display the image correctly unless the user zooms into the page.

Approach

I tried to find a solution that does not require additional markup and is framework agnostic. Just like in an iOS app, the web server should serve the high resolution image, if:

  • the request came from a device with a high resolution display
  • an image following the @2x naming convention is available

The basis for the solution is a cookie that informs the web server about the device pixel ratio in use.

Server configuration

If the cookie indicates a high resolution device, the server should use a high resolution image if one is available. Otherwise, only the standard resolution image should be served. Here is an example configuration for nginx:

# Serves static files in high resolution only if required
location ~ ^(/media/[^\.]+)(\.(?:jpg|png|gif))$ {
  # Naming convention for high resolution images:
  # [filename]@2x[extension], e.g.:
  # example@2x.png
  set $hidpi_uri $1@2x$2;

  if ($http_cookie !~ 'device-pixel-ratio=2') {
    # If the device-pixel-ratio cookie is not set to 2, fall back to
    # default behaviour, i.e. don't try to serve high resolution image
    break;
  }

  # device-pixel-ratio cookie is set to 2
  # Serve high resolution image if available,
  # otherwise fall back to standard resolution
  try_files $hidpi_uri $uri =404;
}

The configuration looks for the device-pixel-ratio cookie. If the cookie is not set to the value 2, no rewrites are performed and the server simply tries to serve the file at the specified location.

If the cookie is set correctly, the server tries to serve a high resolution image. Using nginx's try_files directive, the server first looks for a file that uses the @2x convention. If this is not available, it tries to serve the regular image. If this is not available either, a 404 error is raised.

Setting the cookie without JavaScript

I tried to find a way to set the device-pixel-ratio cookie without requiring JavaScript. This would make the whole solution work for browsers that have JavaScript deactivated. The solution I came up with does not work 100%, however, so a straightforward JavaScript-based alternative is presented afterwards.

The basis of the solution is the CSS device-pixel-ratio media query. In the head of the document, we put this style:

<style type="text/css">
  @media only screen and (-webkit-min-device-pixel-ratio : 2),
       only screen and (min-device-pixel-ratio : 2) {
     
    head {
      /*
        Set device-pixel-ratio cookie
        head tag is invisible but request is still sent
      */
      background-image: url(/set-device-pixel-ratio/2);
    }
  }
</style>

Using the device-pixel-ratio media query, we target high resolution devices. The head tag, which is always present but never visible, has a background-image property set. The URL set for the background image does not actually return a background image. The only purpose of the CSS style is to send a request to the server only if a high resolution device is in use.

Second, the web server needs to be configured to respond to the request sent by the background image property. Again, the example given here is for nginx.

# nginx example configuration

server {
  # Sets the device-pixel-ratio cookie
  location ~ /set-device-pixel-ratio/(\d+)/? {
    add_header Set-Cookie "device-pixel-ratio=$1;Path=/;Max-Age=31536000";
    return 204; # nginx does not allow empty 200 responses
  }
}

All this does is set a cookie named device-pixel-ratio to a provided value. It then returns the cookie inside an otherwise empty response.

The big problem with this approach is the first time a page is loaded. At this moment, the cookie is not yet set. Therefore, requests for the images on the first page are sent without the cookie, and the web server returns the standard resolution images instead. I have not managed to defer the image requests until the request that sets the cookie has returned. For subsequent page loads, the cookie already set, and the approach works.

Setting the cookie with JavaScript

Using JavaScript, setting the device-pixel-ratio cookie becomes straightforward. The cookie needs to set before any requests for images are sent, so the script needs to live inline in the head section of the document.

<script type="text/javascript" charset="utf-8">
if (!document.cookie.match(/\bdevice-pixel-ratio=/)) {
  document.cookie = 'device-pixel-ratio='  
      + (window.devicePixelRatio &gt; 1 ? '2' : '1') + '; path=/';
}
</script>

If the cookie is not set, the script queries the window.devicePixelRatio property and sets the cookie accordingly.

The two strategies can be combined. In this case, even if JavaScript is disabled, most images will be displayed in high resolution. Having the server provide the set-device-pixel-ratio URL also makes it easier to test. It allows you to manually set the device-pixel-ratio cookie to a different value and thus simulate what users with other devices see.

Limitation

This technique does not define how images should be displayed. If a high resolution image is served, it should be displayed half the size of a regular image, in order to achieve the desired effect. If this is not done, the image is simply displayed double the intended size, and the page layout is messed up.

An obvious way is to limit the size using CSS. This however, requires prior knowledge of the size of the image. A better solution would automatically resize the image to half the size whenever a high resolution version is served. I have not yet managed to devise a strategy for this.

Summary

Conditionally serving high resolution images does not require additional markup or additional requests. A cookie can be used to indicate the client device pixel ratio to the server. Based on this information, the server can serve a high resolution image if one is available. Apple's @2x naming convention for images is a convenient way to indicate if a high resolution version is available. The cookie can be set via JavaScript or a server request initiated by CSS, although the latter strategy fails in certain cases.

The strategy described is incomplete but could lead to a useful, generic solution to a problem that will affect a sharply increasing number of users in the coming months and years. Comments and suggestions for improvement are greatly appreciated.

A demo page is available that shows the technique in action. Example code is available as a Gist.

Comments

  1. Daniel W.

    Daniel W. on 04/06/2012 8:16 a.m.

    Can you somehow allow the user to proactively choose low resolution when they do not using a broadband internet connection available?

Comments are closed.

Reactions

Pingbacks

Pingbacks are closed.

Other articles

Next article:

Previous article: