DEV Community

Dane Hillard
Dane Hillard

Posted on • Originally published at easyaspython.com on

Django's cached template loader

In our never-ending aspiration toward better performance in Django, we are constantly looking for ways to do less work in less time while maintaining a consistent experience for our users. It's especially wonderful when we find we can use a built-in approach from the Django tool belt!

Django templates carry two concepts, inclusion and extension, that can result in multiple templates being used to render a single page. A base template may include all the JavaScript, CSS, and skeleton HTML setup you need for your pages. It may include other templates for the navigation and footer, for instance. Other templates may extend the base template to remove certain portions of the base content or add page-specific sections. As a result, it's common for a single page to employ a handful of templates before finally being rendered and sent back to the browser.

As a site grows in features and audience, it's not uncommon for the number and complexity of its templates to grow as well. There may be several layers of extension involved, and template inclusion can go on seemingly indefinitely with nested includes at multiple levels. This can sneak up pretty quickly!

When Django decides it's time to render a page, it determines which templates to use through a series of loaders. The loaders run in order, searching through the templates they know about to see if any of them match the requested template name. Django uses the first template it finds via these loaders, ultimately raising a TemplateNotFound exception if no loaders can find a match. Once Django has decided on a template, it reads that template from disk and follows any further template references it needs to until it has sufficient information to render the current page. This seems pretty straightforward, but there's a catch!

Prior to Django 1.10, the default behavior reads each template needed, from disk, per request, per usage. A template included inside a loop will be read from disk as many times as that loop runs. A particular application of ours receives 500 requests per minute during peak traffic, which is generally manageable. However, some of those requests required hundreds or even thousands of uses of a single template, which put a strain on resources such as I/O and CPU.

The key to solving this is Django's cached template loader. As the other loaders discover the templates the application is asking for, the cached loader will store the template content in memory. The next time that template is requested, the cached loader doesn't need to search for a match and will return the template content directly from memory instead of going out to the disk. The performance of this particular part of our application is no longer so dependent on the number of requests coming in, allowing performance to scale more predictably.

Enabling the cached template loader in Django versions earlier than 1.10 or in applications where the loaders are explicitly listed is easy:

TEMPLATES = [{
    ...
    'loaders': [
        'some.custom.Loader',
        ('django.template.loaders.cached.Loader', [
            'django.template.loaders.app_directories.Loader',
            'django.template.loaders.filesystem.Loader',
        ])
     ]
}]

Simply surround the loaders that you would like cached in Django's django.template.loaders.cached.Loader wrapper! Note that in Django 1.10+ the default setting only turns the cached loader on when DEBUG is False, so to achieve this behavior yourself you will need to add some extra logic in your settings.py. The documentation also warns to use the cached loader only for templates with thread-safe template tags.

Just how effective can this small addition be? Your mileage will almost certainly vary, but we can show you how it affected our application described above.

We enabled the cached template loader in this application just after 9am. The average CPU on the new servers was almost half that of the old servers, with less overall noise. Similarly, the response time for this application dropped nearly 50% and is much more stable. As a result of this change we were able to reduce our server count by one as wellโ€Šโ€”โ€Ša 33% improvement!

If you're running Django <1.10 or you're in 1.10+ and have explicitly listed your template loaders without using the cached loader, consider what performance benefits it could bring for you.

This story originally appeared on Build Smarter.

You may also enjoy my other stories about building stuff in Django.


Top comments (0)