Written by Adrian Holovaty on May 18, 2009

We've launched user accounts at EveryBlock, and we faced the interesting problem of needing to cache entire pages except for the "You're logged in as [username]" bit at the top of the page. For example, the Chicago homepage takes a nontrivial amount of time to generate and doesn't change often -- which means we want to cache it -- but at the same time, we need to display the dynamic bit in the upper right:

One solution would be to pull in the username info dynamically via Ajax. This way, you could cache the entire page and rely on the client to pull in the username bits. The downsides are that it relies on JavaScript and it requires two hits to the application for each page view.

Another solution would be to use Django's low-level cache API to cache the results of the queries directly in our view function. The downsides are that it's kind of messy to manage all of that caching, plus each page view still incurs the overhead of template rendering (which isn't horrible, but it's unnecessary overhead).

The solution we ended up using is two-phased template rendering. Credit for this concept goes to my friend Honza Kral, who suggested the idea to me during PyCon earlier this year.

The way it works is to split the page rendering into two steps:

At cache reset time, render everything except the "You're logged in as" bit, which should remain unrendered Django template code. Cache the result as a Django template. (This is the clever part!)

At page view time, render that cached template by passing it the current user. This is super fast because, at this point, the template only has two or three template tags. (The rest of the page is already rendered.)

It's a clever solution because you end up defining what doesn't get cached instead of what does get cached. It's a sideways way of looking at the problem -- sort of like how Django's template inheritance system defines which parts of the page change instead of defining server-side includes of the common bits.

In order to make this work, we had to write two parts of infrastructure: a template tag and a middleware class that does the cache-checking and rendering. The template tag looks like this:

# Copyright 2009, EveryBlock # This code is released under the GPL. from django import template register = template.Library() def raw(parser, token): # Whatever is between {% raw %} and {% endraw %} will be preserved as # raw, unrendered template code. text = [] parse_until = 'endraw' tag_mapping = { template.TOKEN_TEXT: ('', ''), template.TOKEN_VAR: ('{{', '}}'), template.TOKEN_BLOCK: ('{%', '%}'), template.TOKEN_COMMENT: ('{#', '#}'), } # By the time this template tag is called, the template system has already # lexed the template into tokens. Here, we loop over the tokens until # {% endraw %} and parse them to TextNodes. We have to add the start and # end bits (e.g. "{{" for variables) because those have already been # stripped off in a previous part of the template-parsing process. while parser.tokens: token = parser.next_token() if token.token_type == template.TOKEN_BLOCK and token.contents == parse_until: return template.TextNode(u''.join(text)) start, end = tag_mapping[token.token_type] text.append(u'%s%s%s' % (start, token.contents, end)) parser.unclosed_block_tag(parse_until) raw = register.tag(raw)

This template tag merely treats everything between {% raw %} and {% endraw %} as unrendered template code.

Then, in our base EveryBlock template, we wrap the appropriate bit of code in the {% raw %} tag, like this:

{% raw %} {% if USER %} <p>Logged in as {{ USER.email }}</p> {% else %} <p>Sign in / register.</p> {% endif %} {% endraw %}

The final part is to write some middleware that renders every text/html response through the template system:

# Copyright 2009, EveryBlock # This code is released under the GPL. from django.core.cache import cache from django.template import Template from django.template.context import RequestContext import urllib class CachedTemplateMiddleware(object): def process_view(self, request, view_func, view_args, view_kwargs): response = None if request.method == 'GET' and 'magicflag' not in request.GET: cache_key = urllib.quote(request.path) response = cache.get(cache_key, None) if response is None: response = view_func(request, *view_args, **view_kwargs) if 'magicflag' not in request.GET and response['content-type'].startswith('text/html'): t = Template(response.content) response.content = t.render(RequestContext(request)) return response

One thing to note here is that there's a backdoor for an external process (say, our script that resets the cache) to retrieve the halfway-rendered template code for any page -- magicflag in the query string. (We actually use something different on EveryBlock; I've changed this example.) So that means the only thing the cache-resetting script has to do is make a request to the appropriate page, with that query string, and save the result in the cache. Pretty slick.

There's also a potential gotcha/limitation here: anything within {% raw %} and {% endraw %} will only have access to a template context with the default RequestContext stuff -- which, in our case, will be user-specific stuff.

Thanks again to Honza for telling me about this concept. It's a great idea, and it's serving us well.