As a web developer with a shoddy rural internet connection, I'm always interested in speeding up my sites. One technique for doing this is far-future expires — i.e. telling the browser to cache media requests forever, then changing the uri when the media changes. In this article, I outline how to implement this and several other techniques in django.
UPDATE: I'm now using Django Compressor, which provides the same features but is a superior app.
First up, I installed the django-compress app. I was about to build my own solution when I realised this one did exactly what I needed — gotta love the django community. Configuring is straightforward — the project wiki has articles on installation, configuration, and usage..
I copied the compress/ directory into my django/library/ folder (which is on the python path) and added "compress" to my INSTALLED_APPS.
Once I had django-compress up and running, I had achieved goal #1. To achieve #2 and #3 I needed to configure apache to send the right headers along with each request. To do this, I put the following directive in my httpd.conf file:
<DirectoryMatch /path-to-django-projects/([^/]+)/media>
Order allow,deny
Allow from all
# Insert mod_deflate filter
SetOutputFilter DEFLATE
# Netscape 4.x has some problems...
BrowserMatch ^Mozilla/4 gzip-only-text/html
# Netscape 4.06-4.08 have some more problems
BrowserMatch ^Mozilla/4\.0[678] no-gzip
# MSIE masquerades as Netscape, but it is fine
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
# Don't compress images
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png)$ no-gzip dont-vary
# Make sure proxies don't deliver the wrong content
Header append Vary User-Agent env=!dont-vary
# MOD EXPIRES SETUP
ExpiresActive on
ExpiresByType text/javascript "access plus 10 year"
ExpiresByType application/x-javascript "access plus 10 year"
ExpiresByType text/css "access plus 10 years"
ExpiresByType image/png "access plus 10 years"
ExpiresByType image/x-png "access plus 10 years"
ExpiresByType image/gif "access plus 10 years"
ExpiresByType image/jpeg "access plus 10 years"
ExpiresByType image/pjpeg "access plus 10 years"
ExpiresByType application/x-flash-swf "access plus 10 years"
ExpiresByType application/x-shockwave-flash "access plus 10 years"
# No etags as we're using far-future expires
FileETag none
</DirectoryMatch>
<DirectoryMatch /path-to-django-projects/([^/]+)/media> is equivalent to writing <Directory /path-to-django-projects/site-name/media> for each site.This means that for all my django sites' media directories:
Note you will need mod_deflate and mod_expires enabled in your apache config - if you have apache 2.2 it should just be a matter of copying the relevant files from apache2/mods-available/ to /apache2/mods-enabled/.
Step #4 was the trickiest of the lot, and many would argue that it's not really worth the trouble. Depending on how verbosely you comment your js and css, it may or may not be worthwhile for you — personally, I just thought I may as well go the whole hog. In the end, I probably only saved a few percent worth of bandwidth for my small content sites, but it'll be more significant with js-heavy web-apps.
For js minification, django-compress comes with jsmin built in. I've found this to be ideal for the job, and it is enabled by default.
For CSS, django-compress comes with CSSTidy — a CSS parser and optimiser — built in, in the form of csstidy_python. (You can also use a csstidy binary if you have one installed.) Personally, I find CSSTidy messes with my css, and more significantly, messes with that of my css framework of choice, 960.gs. I was after something that simply stripped whitespace, newlines and comments, without parsing the code. After scouring the web, I came across Slimmer — a lightweight pyhon app that did exactly what I needed. After installing it, I added the following file to the django-compress app's filters directory.
#compress/filters/slimmer_css/__init__.py
import slimmer
from compress.filter_base import FilterBase
class SlimmerCSSFilter(FilterBase):
def filter_css(self, css):
return slimmer.css_slimmer(css)
Then it was simply a matter of adding the following line to my settings.py file, as per the django-compress documentation:
COMPRESS_CSS_FILTERS = ('compress.filters.slimmer_css.SlimmerCSSFilter',)
So my complete django-compress configuration in settings.py was as follows:
# compress app settings
COMPRESS_CSS = {
'all': {
'source_filenames': (
'css/lib/reset.css',
'css/lib/text.css',
'css/lib/960.css',
'css/style.css',
),
'output_filename': 'compress/c-?.css',
'extra_context': {
'media': 'screen,projection',
},
},
# other CSS groups goes here
}
COMPRESS_JS = {
'all': {
'source_filenames': ('js/lib/jquery.js', 'js/behaviour.js',),
'output_filename': 'compress/j-?.js',
},
}
COMPRESS = True
COMPRESS_VERSION = True
COMPRESS_CSS_FILTERS = ('compress.filters.slimmer_css.SlimmerCSSFilter',)
I keep all my css within the <head> tags, and js at the bottom of the page — this is because the web browser needs to download all the css before it can start rendering the page, but doesn't need the js. It doesn't actually speed up the site, but it gives the impression of loading faster, and the user is unlikely to click on anything before the js has loaded anyway.
For a definitive guide, see Yahoo's performance rules. I also recommend Yahoo's YSlow, and if you are one of the 3 remaining web developers without it, Firebug.