Here I'm going to explain a solution I had to make for a site I recently launched. Basically, I wanted to cache the whole page in memcache and set the appropriate Expires
and Cache-Control
headers so that my view was only rendered once an hour and parts of the page needs to be unique (i.e. "Hi, logged in as xxxx")
The advantages is great: The page loads fast, content is stored in memcache every hour, page still appears to be dynamic.
The disadvantages are not so great: the AJAX loads fast but causes a flicker
Basically, I wrote a custom decorator called custom_cache_page(<delay in seconds>)
that works like the normal cache_page(<delay in seconds>)
decorator available in stock Django. However, my decorator inserts a piece of HTML into the rendered HTML (before it's stored in memcache) that I later use to update certain elements of the page with AJAX instead of server side.
Enough talking, let's look at the code!:
from django.utils.decorators import decorator_from_middleware
from django.middleware.cache import CacheMiddleware
class CustomCacheMiddleware(CacheMiddleware):
def __init__(self, cache_delay=0, *args, **kwargs):
super(CustomCacheMiddleware, self).__init__(*args, **kwargs)
self.cache_delay = cache_delay
def process_response(self, request, response):
if self.cache_delay:
extra_js = '<script type="text/javascript">var '\
'CACHE_CONTROL=%s;</script>' %\
self.cache_delay
response.content = response.content.replace(u'</body>',
u'%s\n</body>' % extra_js)
return super(CustomCacheMiddleware, self
).process_response(request, response)
custom_cache_page = decorator_from_middleware(CustomCacheMiddleware)
if settings.DEBUG:
def custom_cache_page(delay):
def rendered(view):
def inner(request, *args, **kwargs):
return view(request, *args, **kwargs)
return inner
return rendered
@custom_cache_page(60 * 60 * 1) # 1 hours
def my_expensive_but_cacheable_view(request):
# only run max. once every hour
complex_and_cpu_intensive_calculation()
...
What you now get is that in every page that is server-side cached in memcache (and set Expires
and Cache-Control
headers) get a little piece of Javascript code inserted into the rendered HTML. Now, all I need to do is to write some jQuery code that loads the navigation menu dynamically but instigate it from a AJAX request. Your mileage here might vary (put this in your base.html or whatever you call it):
$(function() {
if (typeof CACHE_CONTROL != "undefined" && CACHE_CONTROL) {
// the page is cached, need to use AJAX to load what should be dynamic
$('#nav').load('/_nav.html');
}
});
I usually prefix all my views with an underscore when they only return a limited chunk of HTML rather than a whole HTML document.
Discussion
98% of my pages are deliberately not cached because they either aren't expensive to render or really can't be cached because of logged in users or whatnot.
This solution has the added benefit of being totally contained in Django, so it doesn't require any work on the nginx/apache/lighttpd front end but if you really want some speed (e.g. getting nginx talking directly to memcache) you can still use the above solution to get what you want.
My little decorator can be improved admittedly. You might perhaps add a dynamic check if the page should not be cache for example. Or perhaps some other available trick for invalidating the cache if you're really need to. Or perhaps some other tricks can be made to automate the Javascript AJAX loading.
Comments
Thanks for the tip. I liked they way you handled it.
I've seen an approach like this before(ie, loading a small snippet of uncacheable data via js) and your approach is a lot cleaner.