Monday, March 3, 2014

Optimizing Django: tricks for faster page loads

By reducing the file size of your CSS, JavaScript and images, as well as the number of unnecessary browser requests made to your site, load time of your applications pages can be drastically reduced, not to mention the load on your server.
Yahoo have created a list of the 35 best practices to speed up your website, a recommended read for any web developer. I wanted to summarize a few I recently implemented in a Django application.
In a nutshell:
  • Serve compressed static media files. This one is obvious, the smaller the file size, the quicker it is to download.
  • Once a browser has already downloaded the static media, it shouldn't have to download it again. This is what actually happens, as the server will respond with 304 Not Modified, which means that the cached file is still up-to-date.
  • However, the HTTP request is still sent. This is unnecessary and should be avoided by setting the HTTP Expire header to a date in the future.
  • When files are updated, you need to give them a new URL so the browser will request them. This is referred to as versioning. A common scheme is to use the modification time/date of the file in its URL, either as part of the name or in the query string. This is obviously tedious and error prone, so automation is required.
     

Django-Compress (CSS/JS compression and auto-versioning)

django-compress provides an automated system for compressing CSS and JavaScript files, with built in support for auto-versioning. I was pleasantly surprised by the ease of integration, and was up and running within a couple of minutes.

Installation

Firstly, django-compress depends on csstidy, so lets install it.
apt-get update
apt-get install csstidy
Now, grab the latest tarball from github.
wget http://github.com/pelme/django-compress/tarball/master
Unpack and add it to your site (or the Python path).
tar -zxf pelme-django-compress-*.tar.gz
cp -a pelme-django-compress-*/compress /path/to/django/apps/compress
Update settings.py to enabled django-compress, auto-updates, and versioning.
# settings.py

COMPRESS = True
COMPRESS_AUTO = True
COMPRESS_VERSION = True
...

INSTALLED_APPS = (
    ...
    'compress',
)

Configuration

Configure which CSS and JavaScript files to compress and auto-version.
# settings.py
COMPRESS_CSS = {
    'screen': {
        'source_filenames': ('css/style.css', 'css/foo.css', 'css/bar.css'),
        'output_filename': 'compress/screen.?.css',
        'extra_context': {
            'media': 'screen,projection',
        },
    },
}

COMPRESS_JS = {
    'all': {
        'source_filenames': ('js/jquery-1.2.3.js', 'js/jquery-preload.js')
        'output_filename': 'compress/all.?.js',
    },
}
Note: The '?' will be substituted with the epoch time (I.e., the version).

I hate hardcoding, it's just too error prone and not scalable, so I used this primitive helper function to auto-generate the required configuration.
# utils.py
import os
# generate config for django-compression
# alpha-numeric ordering for customization as required
def compress_cfg(media_root, dir, output):
    files = os.listdir(os.path.join(media_root, dir))
    files.sort()
    return {'source_filenames': tuple(os.path.join(dir, f) for f in files),
            'output_filename': output}
Using the above helper function, the hardcoded files can be replaced with the following for auto-generation (after MEDIA_ROOT is defined).
# settings.py
from utils import compress_cfg...

COMPRESS_CSS = {'screen': compress_cfg(MEDIA_ROOT, 'css', 'compress/screen.?.css')}
COMPRESS_JS  = {'all': compress_cfg(MEDIA_ROOT, 'js', 'compress/all.?.js')}

Usage

Now you can update your templates to use the compressed auto-versioned files, for example:

{% load compressed %}
<html>
    <head>
        {% compressed_css 'screen' %}
        {% compressed_js 'all' %}
    </head>
    ...


Image optimization and versioning

With regards to image optimization, take a look at Liraz's post on PNG vs JPG: 6 simple lessons you can learn from our mistakes.
It would be great if django-compress supported image versioning, but it currently doesn't.
I found this nice snippet which provides a template tag for asset versioning, such as images, but it only solves half the problem, the other half being images specified in CSS.
If you need to go the image versioning route, a more complete solution is django-static, which also does CSS/JS compression, though I prefer django-compress.
Currently, I have not implemented image versioning. Its just not worth the complexity as I don't have too many images, and have no plans to change them, often, hopefully. The Expires Header is good enough for now. (Pre-mature optimization is the root of all evil).

HTTP Expire header

As discussed above, without an Expires header, the browser will request media files on every page load, and will receive a 304 Not Modified response if the cached media files are up-to-date.
Setting a Far Future Expire is possible, and recommended, now that you have versioned CSS and JavaScript files.
In the following examples I have set images to expire 2 hours after they are accessed, but you can tweak this to your specific use case. CSS and JavaScript (basically) never expire.
Depending on your media server, add your custom configuration, enable the expire(s) module, and reload the webserver.
Apache2
ExpiresActive On

ExpiresByType image/jpg "access plus 2 hours"
ExpiresByType image/png "access plus 2 hours"
...

ExpiresByType text/css "access plus 10 years"
ExpiresByType application/x-javascript "access plus 10 years"
Lighttpd
server.modules = (
    "mod_expire",
    ...

$HTTP["url"] =~ "\.(jpg|png|gif|ico)$" {
    expire.url = ("" => "access plus 2 hours")
}

$HTTP["url"] =~ "\.(css|js)$" {
    expire.url = ("" => "access plus 10 years")
}
 Thank for waching !

No comments:

Post a Comment