Also published on design.canonical.com.
It's becoming more and more important for websites to carefully consider how their resources are cached in users' browsers. Get the caching wrong, and you either end up with a woefully slow experience for the user, or a very strange looking website as users are left with stale CSS files and images.
Or often both.
Caching headers in Django
Telling the browser how long to cache a resource is done with one of two headers:
Cache-Control: In HTTP/1.1, this can set the maximum age before a resource should be re-downloaded.
Expires: In the older HTTP/1.0 standard, this sets the date and time that a resource becomes outdated and should be refreshed.
To control these headers in Django is less simple than you might think. If you're happy to use the cache framework then it will take care of these headers for you, but as we have a separate Squid cache in front of our application, this was a more heavyweight solution than we needed.
Modifying HTML responses using View classes
In our case, all of our HTML pages are served with an extended version of the TemplateView class:
from django.views.generic.base import TemplateView class OurTemplateView(TemplateView): # Setup our custom template data
To add headers, we need to modify the
HTTPResponse, which we can intercept
by extending the
Django also provides
patch_response_headers a handy helper function to generate our caching headers for us
and attach them to the response:
class OurTemplateView(TemplateView): def render_to_response(self, context, **response_kwargs): # Get response from parent TemplateView class response = super(CmsTemplateFinder, self).render_to_response( context, **response_kwargs ) # Add Cache-Control and Expires headers patch_response_headers(response, cache_timeout=300) # Return response return response
And now we can see our extra caching headers in the HTTP response:
$ curl -I cn.ubuntu.com ... Date: Fri, 12 Feb 2016 22:48:38 GMT Expires: Fri, 12 Feb 2016 22:53:35 GMT Last-Modified: Fri, 12 Feb 2016 22:48:35 GMT Cache-Control: max-age=300
Browsers and proxies will now cache the HTML pages for 5 minutes.
Controlling caching for static files
Django recommends serving static files separately from the rest of your application.
However, for simplicity and dev-prod parity we've been using DJ-Static to serve static files with the Django WSGI app, as introduced by Kenneth Reitz. This was also, at the time we implemented it, the method recommended by Heroku for managing static files in Django.
However, as it turns out DJ-Static doesn't offer any control over caching headers. And Heroku now recommend using WhiteNoise instead.
Serving static files with WhiteNoise is pretty simple (as it was with DJ-Static):
# myapp/settings.py STATIC_ROOT = 'static' STATIC_URL = '/static/' # myapp/wsgi.py from django.core.wsgi import get_wsgi_application from whitenoise.django import DjangoWhiteNoise application = DjangoWhiteNoise(get_wsgi_application())
WhiteNoise will add a
Cache-Control header, although it doesn't
support set the older
Expires header. By default, the
is initially set to no caching:
$ curl -I localhost:8000/static/css/styles.css?v=d5d2934 ... Cache-Control: public, max-age=0
We wanted our static files to be cached for a year, so we set the
WHITENOISE_MAX_AGE setting in
# myapp/settings.py WHITENOISE_MAX_AGE = 31557600
This will set the
max-age in the
Cache-Control header to achieve the
browser caching we're looking for:
$ curl -I http://cn.ubuntu.com/static/css/styles.css?v=d5d2934 ... Cache-Control: public, max-age=31557600
Now we have control
Leveraging browser caching is an invaluable tool in performance, and so understanding how we can control the user's cache with Django is very helpful.
Hopefully I've demonstrated some ways that this can be achieved, which we've just implemented on cn.ubuntu.com.