Adaptive Images

Deliver small images to small devices

Here you can find out how Adaptive Images works, how to install it, how to customise it for your needs, and learn about the limitations of the approach.

How it works

Adaptive Images does a number of things depending on the scenario the script has to handle but here's a basic overview of what happens when you load a page (assuming Apache, though nginx isn't much different):

  1. The HTML starts to load in the browser and a snippet of JS in the <head> writes a session cookie, storing the visitor's screen size in pixels.
  2. The browser then encounters an <img> tag and sends a request to the server for that image. It also sends the cookie, because that’s how browsers work.
  3. Apache receives the request for the image and immediately has a look in the website's .htaccess file, to see if there are any special instructions for serving files.
  4. There are! The .htaccess says "Dear server, any request you get for a JPG, GIF, or PNG file please send to the adaptive-images.php file instead."

The PHP file then does some intelligent thinking which can cover a number of scenario's but I'll illustrate one path that can happen:

  1. The PHP file looks for a cookie and finds that the user has a maximum screen size of 480px.
  2. It compares the cookie value with all $resolution sizes that were configured, and decides which matches best. In this case, an image maxing out at 480px wide.
  3. It then has a look inside the /ai-cache/480/ folder to see if a rescaled image already exists.
  4. We'll pretend it doesn’t - the PHP then goes to the actual requested URI to find the original file.
  5. It checks the image width. If that's smaller than the user's screen width it sends the image.
  6. If it's larger, the PHP creates a down-scaled copy and saves that into the /ai-cache/480/ folder ready for the next time it's needed, and sends it to the user.

It also does a few other things when needs arise, for example:

  • Detects Retina displays. You can choose to serve high DPI images to those displays if you want, by using an alternate line of JavaScript.
  • It sends images with a cache header that tells proxies not to cache the image whilst telling browsers they should. This avoids problems with proxy servers and network caching systems grabbing the wrong image and storing it.
  • It handles cases where there isn't a cookie set; mobile devices will be supplied the smallest image, and desktop devices will get the largest.
  • It compares time-stamps between the source image and the generated cache image - to ensure that if the source image gets updated, the old cached file won’t be sent.

Common Questions

There are a couple of very common things people ask about, so to be perfectly clear:

  1. Adaptive Images does NOT work on your browser window size, resizing your browser will have no effect on the size of images that are downloaded. It works on the screen size. The reason for this is to avoid poor caching behaviour; if you're on a large screen with a small window and AI were to deliver the small image, that image would be cached by your browser. As soon as you maximised the window the image would obviously be too small, and the browser doesn't know it needs to re-request the image.
  2. To support Retina displays all you need to do is use the alternate JS. Nothing more.
  3. AI works perfectly fine with WordPress; but you'll have to configure your .htaccess file yourself, and that means you need to know a little about how WordPress works. Try following this guide to getting Adaptive Images working with WordPress

Installation

For Apache:

One important thing: do not over-write any existing .htaccess file. If you have one already, back it up. Feeling up to it? Excellent:

  1. Download the latest version of Adaptive Images either from the website or from the GitHub repository.
  2. Upload the included .htaccess and adaptive-images.php files to the server document-root.
  3. Add this line of JavaScript as high in the <head> of your site as possible, before any other JS:


    <script>document.cookie='resolution='+Math.max(screen.width,screen.height)+'; path=/';</script>

  4. Configure the $resolutions variable at the top of the adaptive-images.php file to match the breakpoints you are using for your designs.

If you already have an .htaccess file, open it in a text editor and add the code from the supplied file into your existing one.

For nginx:

nginx does not use .htaccess files, so do as above but replace Step 2 with the following:

In your virtual-host config file, inside the server block, ensure you've got the following:

    location assets {
    }
    location ai-cache {
    }
    
    location / {
        rewrite \.(?:jpe?g|gif|png)$ /adaptive-images.php;
    }

You'll need to ensure you've got PHP running with nginx too, but that's outside of the scope of this site. Embrace your GoogleFu.

Customising

If you don't like the default values there is a lot that can be changed:

adaptive-images.php

By looking in the configuration section at the top of the PHP file you can:

  1. Set breakpoints to match your CSS's Media Queries

    $resolutions = array(1382, 992, 768, 480);

    If you're doing a responsive design then you ought to make these values the same as your CSS's. You can have as many or as few as you'd like, e.g.,

    $resolutions = array(1200, 768, 480);

  2. Change the name and location of the ai-cache folder

    $cache_path = "ai-cache";

    This path is relative to the server root, you can change it to whatever you like as long as you provide a valid path and directory name, e.g.,

    $cache_path = "subdirectory/my-website/adaptive-images/cache";

  3. Change the quality any generated JPG images are saved at

    $jpg_quality = 80;

    Quality can be any number from 0 to 100, with 100 being the best (but with largest file-size). e.g.,

    $jpg_quality = 50;

    Note that you can not control the quality at which GIF or PNG images are saved

  4. Toggle whether to compare the files in the cache folder with the originals or not

    $watch_cache = TRUE;

    Adaptive Images will check that the image in the ai-cache folder is newer than the one from which it was generated. This ensures that if the original image is updated (for example, by someone using a CMS) the ai-cache version will also be updated on the next requested. This means you don't have to manually clear the ai-cache in order to get updated assets. You can turn this off if you like, which may give a slight performance boost:

    $watch_cache = FALSE;

  5. Set how long the browser should cache images for

    $browser_cache = 60*60*24*7;

    Browsers want time values in seconds, but it's easy to work out more friendly numbers. In this case the first number is seconds (60), the second minutes (60), the third hours (24), and the fourth days (7). You only need pay attention to the last number in the sum. The default is therefor 7 days. You can change that to one year like this:

    $browser_cache = 60*60*24*365;

    Or 2hrs would look like this:

    $browser_cache = 60*60*2;

  6. Perform a subtle sharpen on re-scaled images to help keep detail

    $sharpen = TRUE;

    When set to TRUE Adaptive Images will perform a subtle sharpening on any images that it rescales. You can turn that off if you like:

    $sharpen = FALSE;

.htaccess

You probably want to omit a few folders from the AI behaviour. You don’t need or want it re-sizing the images you’re using in your CSS for example. That’s fine - that's what the .htaccess file is for. There are a lot of things you could do here, it is just a standard Apache .htaccess file using ModReWrite, but to help you understand I'll explain what the default values are doing:

RewriteCond %{REQUEST_URI} !assets

RewriteRule \.(?:jpe?g|gif|png)$ adaptive-images.php

The first line is essentially an "if" check. It means "if the requested URI does not have the string 'assets' in it, then carry on to the next line".

The second line is more complex but says "if the end of the URI has '.jpg' or '.jpeg' or '.png' or '.gif', then load adaptive-images.php"

So, this is what the default set-up means:

If the requested file is not inside a directory called "assets" and is either a PNG, GIF, or JPG, do the fancy Adaptive Images stuff.

That means we've protected any images inside a directory called assets, no images in there would have the Adaptive Images processing done on them. You can add as many lines like this as you need.

Or, you can flip it around and say "only images inside of a directory called assets should get the Adaptive Images behaviour":

RewriteCond %{REQUEST_URI} assets

Note the lack of exclamation mark.

If you choose to install the adaptive-images.php file somewhere other than your site root you will need to change the second line to point to wherever you've put the file. e.g.,

RewriteRule \.(?:jpe?g|gif|png)$ server-root/my-website/adaptive-images/adaptive-images.php

JavaScript

If you want to take advantage of high pixel density displays such as the iPhone 4 and iPad 3, use this alernate JavaScript:

<script>document.cookie='resolution='+Math.max(screen.width,screen.height)+("devicePixelRatio" in window ? ","+devicePixelRatio : ",1")+'; path=/';</script>

A word of caution: such images will look very sharp, but they'll still be pretty big and slow to load.

Limitations

I think this is one of the most flexible, future-proof, retro-fittable, and easy to use solutions available today. But, there are problems with this approach as there are with all of the one’s I’ve seen so far. In the case of Adaptive Images they are these:

This is a PHP solution.

I wish I was smarter and knew some fancy modern languages the cool kids discuss at parties, but I don’t. So, you need PHP on your server. That said, Adaptive Images is Creative Commons and I would welcome anyone contributing a port of the code.

Content Delivery Networks.

Adaptive Images relies on the server being able to intercept requests for images, do some logic, and then send one of a given number of responses. Content Delivery Networks are generally dumb-caches, and they won’t allow that to happen. Adaptive Images will not work if you’re using a CDN to deliver your website.

As Yoav Veiss pointed out in his article Pre-loaders, Cookies, and race-conditions there is no way to guarantee that a cookie will be set before images are requested - even though the JavaScript that sets the cookie is loaded by the browser before it finds any <img> tags. That could mean images being requested without a cookie being available. Adaptive Images has a mechanism to avoid this being too much of a problem:

Adaptive Images checks the User Agent String for the presence of 'mobile'. This means that if the user was unlucky (their browser didn't write the cookie fast enough on the first visit to the website, or cookies are disabled) mobile devices will be supplied the smallest image, and desktop devices will get the largest.

The best way to get a cookie written is to use JavaScript as I’ve explained above, because it's the fastest. However, for those that want it there is a no-javascript method which uses CSS and a bogus PHP "image" to set the cookie. A word of caution: because it requests an external file this method is slower than the JS one, and it is very likely that the cookie won’t be set on the first visit to the website.

The Future

For today, this is a pretty good solution. It works, doesn't interfere with your mark-up, and the process is "non-destructive". If a future solution is superior you can just remove the Adaptive Images files and you'd never know AI had been there.

However this isn't really a long-term solution, not least because of the cookie race condition and CDN/proxy-cache incompatibility. What we really need are a number of standardised ways to handle this in the future.

Firstly we could do with browsers sending far more information about the user's environment along with each request (the device size, the connection speed, the pixel density, etc) because the way things work now is no longer fit for purpose. Browser makers are reluctant to do this with HTTP, but maybe SPDY will help with it's gzip-compressed headers. We need these headers because the web is a much broader entity used on far more diverse devices than when these technologies were dreamed up. Relying on cookies to do this job is not cutting it, and the User Agent String is a complete mess no longer fit for the varying purposes for which we are forced to hijack it. Vendors argue that larger headers slow page load times. We are at a stage where we as a community need to decide if the pay-off of a few milliseconds latency reduction is worth neutering the potential benefits or having better headers. Let's not forget that for the case of images, although latency may rise, file sizes will drop substantially - making the site considerably faster.

Secondly, we need a W3C backed mark-up level solution for when we need to supply semantically different content at different resolutions, not just re-scaled versions of the same content, as Adaptive Images does. These seem superficially like solutions to the same problem. They are not, and we need both. Bruce Lawson's blog post Notes on Adaptive Images (yet again!) expands on this aspect of the problem.