Serving both image & metadata in 1 requestTags: javascript, web — 5th of March 2011

The What?

Let’s say you want to dynamically display an image on your site, and there’s data associated with this image that you also want to acquire.
Here are 2 scenarios:
  • Your site uses Latex equation images and you want to use CSS to vertically align the image with the surrounding the text (Detail).
  • You are Google and you want to display Instant Preview image of a site along with some metadata.
Both scenarios can easily be solved my making 2 HTTP requests, 1 for the image and 1 for the metadata, but that’s not interesting.

The Neat Trick

We want to load both the image & metadata in only 1 HTTP request and here's how.
  1. Request to load your image using a simple <img /> tag or javascript.
  2. Make the HTTP Response for the image request add the metadata you are looking for to the HTTP Set-Cookie header.
  3. Once the image is loaded on the client’s browser, use javascript to access the browser's cookies to retrieve the metadata returned by the image request.
  4. You can now delete the cookie for cleanup.
So now you have both the metadata and the image loaded and it took only 1 HTTP request.

Example HTTP Response:
HTTP/1.0 200 OK
Content-Length: 451
Content-Type: image/jpeg
Expires: Fri, 30 Oct 2020 13:19:41 GMT
Set-Cookie: metadata=MYDATA; Domain=.sprklab.com; Path=/

[BINARY IMAGE INFORMATION]

How Google can make Instant Preview faster

Google seems to have encountered the same problem. They are trying to load their Instant Preview image with some metadata in 1 HTTP Request. But they took a different approach, instead of having the server return the image binary, the server returns a JSON data that includes the actual image encoded in base64. Then they use Data URI Scheme to load the image.

But this solution has 3 downsides:
  • Base64 encoding causes the image data to be 33% larger
  • Loading image from JSON string is slower than loading an image binary
  • Not all browsers support Data URI Scheme, including IE7. (IE8 limits to 32kb)
By using the trick detailed in this note, Google can easily improve their performance for Instant Preview.

Demonstration



Server side: (Django)
from django.conf import settings
from django.http import HttpResponse
from StringIO import StringIO
import random
import Image, ImageFont, ImageDraw

def get_random_image(request):
	number = str(random.randint(1, 9999)) 

	# Pick a random font to use
	fontslocation = settings.MEDIA_ROOT + "notes/captcha/"
	fonts = os.listdir(fontslocation)
	font = ImageFont.truetype(fontslocation + random.choice(fonts), 25)

	# Draw the Image
	image = Image.new("RGBA", (80, 60), (255,255,255, 0))
	draw = ImageDraw.Draw(image)
	draw.text((0, 0), number, font=font, fill="#000000")
	data = StringIO()
	image.save(data, format="PNG")
	data.seek(0)
	image_data = data.read()

	# Create our Response
	response = HttpResponse(image_data, mimetype="image/png")

	# Append the metadata as a cookie
	key = request.GET["key"]
	response["Set-Cookie"] = "%s=%s; Domain=.sprklab.com; Path=/" % (key, number) 
	return response

Client side:
function getCookie(c_name) {
	if (document.cookie.length <= 0)
		return "";
		
	c_start = document.cookie.indexOf(c_name + "=");
	if (c_start == -1)
		return "";
		
	c_start = c_start + c_name.length + 1;
	c_end = document.cookie.indexOf(";", c_start);
	if (c_end == -1)
		c_end = document.cookie.length;
	return unescape(document.cookie.substring(c_start, c_end));
}

function deleteCookie(name) {
    document.cookie = name + '=; Domain=.sprklab.com; Path=/; expires=Monday, 19-Aug-1996 05:00:00 GMT';
}

// Loads an image given a image source.
// on_success is a callback with the parameters (ImageElement, metadata)
function loadMetaImage(src, onsuccess) {
	var img = new Image();
	
	// Give the image a random cookie name to avoid name conflict when loading multiple images at once
	var key = "mykey_" + Math.ceil(Math.random() * 99999);
	img.src = src + "?key=" + key;
	
	img.onload = function() {
		// When the image is loaded, the cookie should include our meta data
		var metadata = getCookie(key);
		deleteCookie(key);

		onsuccess(img, metadata);
	};
}

// Demo usage
function loadRandomNumber() {
	loadMetaImage("/ext/mathcaptcha_cookie", function(img, metadata) {
		$("#image_container").empty().append(img);
		$("#metadata_container").html(metadata);
		
		$("#demo_container").show();
	});
}