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" &amp;&amp; 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

Tiago S.

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.

Your email will never ever be published.

Related posts