Make inline images to improve performance (with django template tags)

Despite we have now big connections and broadband lines, it’s still too long. So, as developpers, we have a role to play in web-performances.

Today, I’ll show you a snippet I have written a couple of weeks ago, to encode images in base64 on-the-fly.

Click here to go directly to the django part


Why encode images ?

You can b64-encode images to reduce your numbers of HTTP requests. A request has an average response time of 200ms and most of the browsers let you open 2 HTTP connections at the same time. Great famous tools/patterns, like YSLOW and Page speed recommend to reduce the number of HTTP request on a page.

There are various school on how to handle the image case, I know 2 :

Using css-sprites:

It’s a technique which consist to combine all you site images in one image, and then shift the visible area to the required coordonates of the big image.

How I do that ?

You can make your own big sprite by collecting all your site images and then pasting them on a big png or you can use sprites generators (Spritegen, csssprites.com, google).

After that you use the background-position attribute on a tag instead of writting a <img /> tag.  Which gives you pretty much this:


.my-bg-class  {background-image:url('../images/my_sprite.png')}
.info-icon    {background-position:0px -50px}
.warning-icon {background-position:0px -60px}
.error-icon   {background-position:0px -70px}

<div class="my-bg-class info-icon"></div>
<div class="my-bg-class warning-icon"></div>
<div class="my-bg-class error-icon"></div>

So, for displaying 3 icons, you’ll make only one HTTP request and then css handle the rest ! Just for 3 icons you can gain maybe 500ms !

Is that good to use css-sprites ?

Most of time yes, but there are some disadvantages of this technique. When number of images grows the sprite grows at the same time and even if sprite let you make only one HTTP request you’ll to have a big one !

 

Using image base64 encoded

How i do that ?

There are 2 ways to do that (at least I know):

inlining the src in the <img /> tag (for reading purposes I return to the line but your b64 code has to be in one line without “\n”) :

<img src="data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcp
LDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAeAB4D
ASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAk
M2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG
x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx
BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaX
mJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD2XUdSnGofYYkZFAV3kBwSDngflTHjneRW
a7LThRgMAQuO4AxWL4/u73SI4NQspIQSwWRZQSCoOD/6ED+Fc3prvqHi6HUZdTmkkt4QXWEDy36gA+hO7oOu0ZrwcTUqRrT9pOy6fLZadzvp0ealzwW256Np+qxT+d5k
42qcqzLtBA4OPXB4rUVldQykFSMgjvXnkV5e20jm6tJTJGGZWePaACeW7gdRnk4rrtBmd7IRExskKqoaMHGcc/0/OunA491Zexmte/cyrUORcyehY1eOObS545VUow2n
Kg7QTgnn0BrjdJktbCzkt7W2ihmic7uOW5zye3pXc3k8dtaSSyqXQDBUDOc8f1rzDXreC80/7LY6bBZzXVx5Im+0u21QxBwMccr+X6LMqDqWfPb5XLwjveNrm7HrMWs3
BsreNJrqEiTySQcDpk9iOea7GxtI7Gzjt40VQo52jgnua4HRp1Pi9L8RBYo1TTEQHJUbQc/99GvRqvLqFOClKMm/X9CcVdNRasf/2Q==" alt=":'(" />

Or in your css:

.chocobo { background :url(data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBw
gJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI
yMjL/wAARCAAeAB4DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDK
BkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrK
ztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQ
AAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goO
EhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD2XUdSnGofYYkZFAV
3kBwSDngflTHjneRWa7LThRgMAQuO4AxWL4/u73SI4NQspIQSwWRZQSCoOD/6ED+Fc3prvqHi6HUZdTmkkt4QXWEDy36gA+hO7oOu0ZrwcTUqRrT9pOy6fLZadzvp0ea
lzwW256Np+qxT+d5k42qcqzLtBA4OPXB4rUVldQykFSMgjvXnkV5e20jm6tJTJGGZWePaACeW7gdRnk4rrtBmd7IRExskKqoaMHGcc/0/OunA491Zexmte/cyrUORcye
hY1eOObS545VUow2nKg7QTgnn0BrjdJktbCzkt7W2ihmic7uOW5zye3pXc3k8dtaSSyqXQDBUDOc8f1rzDXreC80/7LY6bBZzXVx5Im+0u21QxBwMccr+X6LMqDqWfPb
5XLwjveNrm7HrMWs3BsreNJrqEiTySQcDpk9iOea7GxtI7Gzjt40VQo52jgnua4HRp1Pi9L8RBYo1TTEQHJUbQc/99GvRqvLqFOClKMm/X9CcVdNRasf/2Q==);}

Special attention on web browser support :

Base64 encoded images (and data URIs more generally)  are supported in:

  • Firefox 2+
  • Safari – all versions
  • Google Chrome – all versions
  • Opera 7.2+
  • Internet Explorer 8+

For IE < 8, you need to interest yourself in the MHTML alternative.

Is that good to b64-encode images ?

It depends on your needs, but it can be usefull if you haven’t to support old browsers and if you have small images. Base64 is pretty verbose and big (~30% bigger) but you haven’t to make a HTTP request and if you gzip your css file it is pretty much the same than a regular image.

 

 

Using image base64 encoded in django

As I said previously, I have written a django template tag which do the work of auto-inlining your images stored in your media directory and make a cached version of the image on the server.

You need to have:

  • PIL installed
  • A writable dir for cached version (in the code I used /tmp)

 

File image_tags.py:

# -*- coding: utf-8 -*-
"""
    django templates tags for images
    @author: R. BECK
    @version: 0.1
"""

#from __future__ import with_statement #Python 2.5

import base64
import os
import Image
from re import compile as compile_re
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO  import StringIO

from django.conf     import settings
from django.template import TemplateSyntaxError, Node, Library, Template

register = Library()

TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG
MEDIA_ROOT     = settings.MEDIA_ROOT
CACHE_DIR      = "/tmp"

templatevar_re = compile_re("\{\{(.+)\}\}")
media_re       = compile_re("\{\{MEDIA_ROOT|MEDIA_URL\}\}")

class EmbeddedImgNode(Node):
    """Image node parser for rendering html inline base64 image"""

    def __init__(self, attributes):
        self.attrs = {}
        attrs = self.attrs
        for attr_value in attributes:
            try:
                attr, value = attr_value.split('=', 1)
            except ValueError, val_err:
                raise TemplateSyntaxError(u"Syntax Error :", val_err)
            attrs[attr] = value

        src = attrs.get('src')
        if not src:
            raise TemplateSyntaxError(u"You have to specify a non-empty src \
                                        attribute")

    def _encode_img(self, file_path):
        """Returns image base64 string representation and makes a cache file"""
        filename   = file_path.rpartition(os.sep)[2]
        need_cache = True
        content    = ""

        cache_file = "%s_cache" % os.path.join(CACHE_DIR, filename)

        try:
            with open(cache_file, 'r') as cached_file:
                content = cached_file.read()
            need_cache = False
        except IOError:
            pass

        if need_cache:
            try:
                image = open(file_path, 'r')
                out   = StringIO()
                base64.encode(image, out)
                content = out.getvalue().replace('\n', '')
            except IOError:
                pass
            else:
                try:
                    with open(cache_file, 'w+') as cached_file:
                        cached_file.write(content)
                except IOError:
                    pass
        return content

    def _render_img(self):
        """Prepare image attributes"""
        attrs = self.attrs
        attrs_get = attrs.get
        src    = attrs_get('src')
        height = attrs_get('height')
        width  = attrs_get('width')

        if media_re.search(src):
            src = src.replace('{{MEDIA_ROOT}}', '') \
                     .replace('{{MEDIA_URL}}', '')

        src = src.replace('"', '')
        src = os.path.join(MEDIA_ROOT, src)

        try:
            img     = Image.open(src)
            _width, _height    = img.size
            _format = 'image/%s' % img.format

            if not height:
                attrs['height'] = '%spx' % _height

            if not width:
                attrs['width'] = '%spx' % _width

            b64encoded = self._encode_img(img.filename)

            attrs['src'] = "data:%s;base64,%s" % (_format, b64encoded)
        except IOError:
            attrs['src'] = ""

    def render(self, context):
        self._render_img()

        attrs  = self.attrs
        search = templatevar_re.search

        for k, v in attrs.iteritems():
            if search(v):
                attrs[k] = Template(v).render(context)

        return """<img %s/>""" % ' '.join(['%s=%s' % (k, v if v else '""')
                                             for k, v in attrs.iteritems()])

@register.tag(name="embedded_img")
def do_embedded_img(parser, token):
    return EmbeddedImgNode(token.split_contents()[1:])


How to use it:

  • Put the image_tags.py in templatetags dir of one of your installed app
  • In your template load it

You can use context variables in the template tag.

Your template.html:

{% load image_tags %}
{% embedded_img src="/media/images/002.gif" height="200px" width="200px" alt="{{my_var}}" %}
{% embedded_img src="{{MEDIA_URL}}upload/images/001.jpg" height="200px" width="200px" alt="lala" %}
{% embedded_img src="upload/images/logo_corp_264.png" alt="corp" %}
{% embedded_img src="{{MEDIA_URL}}upload/images/001.jpg" width="200px" alt="lala" %}
{% embedded_img src="{{MEDIA_URL}}upload/images/001.jpg" height="200px" width="200px" alt="lala" %}
{% embedded_img src="{{MEDIA_URL}}upload/images/001.jpg" %}

That’s it ! The template tag generates all for you ! You can inline all images you want in your HTML, even I recommend you to inline only small images.

If you find any bug in my code, or if you think there are some optimisations possible don’t hesitate to comment it !

Those snippets are now available at:

This entry was posted in django, python, web. Bookmark the permalink.

Leave a comment