Thomas Fuchs
Hi, I'm Thomas Fuchs. I'm the author of Zepto.js, of script.aculo.us, and I'm a Ruby on Rails core alumnus. With Amy Hoy I'm building cheerful software, like Freckle Time Tracking and Every Time Zone and write books like Retinafy.me.
   Want me to speak at your conference? Contact me!

Internet Explorer and AJAX image caching woes

August 28th, 2005

Inserting chunks of HTML via AJAX is cool stuff, and speeds things up quite a bit. As long as you don’t happen to insert some images and you’re using Internet Explorer 6, that is.

Here’s the problem: Internet Explorer forgets to look into its own cache when inserting HTML via JavaScript DOM manipulation (read: if you use img tags or any tags with CSS background images, Internet Explorer will always try to redownload these images). Read the detailed account on this.

Microsoft says it’s designed that way. Well. Sure.

Also, the Cache-Control header, if set to something like private, max-age=86400 is not handled too well, as Internet Explorer will always try to revalidate the file (Safari and Firefox don’t, they will use their cached copy until the time given by max-age has elapsed, and only then will do the cache revalidation).

A workaround for this (not perfect, but as good as it gets) is to send proper Last-Modified, Cache-Control, and ETag HTTP headers, so Internet Explorer sends back an If-Modified-Since header trying to find out if it should refresh its cache. We can then just answer with a 304 Not Modified HTTP status, and IE will use the cached image anyway. (Note that IE suddenly seems to “remember” it has the image in the cache here!).

Of course, no Internet Explorer bug comes without proprietary extensions—and these should be used here to to get proper performance (and yes we’re breaking the HTTP standard, but let’s be pragmatic, ok?). Note that is the only way to tell IE to cache like the good guys do.

The ETag header allows for reusability of the cached content across sessions. (Note that basing the ETag on the file modification timestamp can be considered “weak”, because a file might be changed more than once a second. If you serve files that change that often, it’s probably better not to use caching to begin with).

If you’re using Ruby on Rails, here’s a piece of code that wraps around the send_file method (loosely based on this Rails wiki entry) and does the work for you (you’ll need edge Rails for this):

def send_file_cached(path, options = {})
  unloaded = false
  begin
    since = request.env['HTTP_IF_MODIFIED_SINCE']
    since = Time.httpdate(since) rescue Time.parse(since)
  rescue
    unloaded = true
  end
  modified = File.mtime(path)
  headers['ETag'] = modified.to_i.to_s
  headers.delete("Cache-Control")
  if unloaded or (since < modified)
    if options[:expires_in]
      expires_in options[:expires_in]
      # make IE behave
      if (request.env['HTTP_USER_AGENT'] || "").include? "MSIE"
        headers['Cache-Control'] =
          "post-check=#{options[:expires_in]}, " +
          "pre-check=#{options[:expires_in]}"
      end
      options.delete(:expires_in)
    end
    headers['Last-Modified'] = CGI::rfc1123_date(modified)
    send_file path, options
  else
    render :nothing => true, :status => '304 Not Modified'
  end
end

Call that from your action like this:

send_file_cached "path/to/file",
  :type => "mime/type", :expires_in => 2.weeks,
  :disposition => "inline"

If you’re on fastcgi on Apache like me, you’ll need to add

-pass-header If-Modified-Since

to your FastCgiServer directive in your Apache configuration. If you are on an other configuration, you also may need to adjust request.env['HTTP_IF_MODIFIED_SINCE'] to the correct one in your enviroment. Do a request.env.inspect to find out the name of the If-Modified-Since variable.

This sit-up should make for reasonably fast speeds with Internet Explorer and served-by-your-app images, with or without AJAX.

P.S. There’s an other bug in Intenet Explorer (unrelated?) that causes caching of images in CSS only happen with relative paths, so never use background-image:url(/images/blah.png) but do a background-image:url(../images/blah.png), relative to the path your CSS file is in.