Speed Up Django Template Rendering With the Cached Loader

Make Django templates render faster in production: confirm the cached template loader is active, avoid disabling it with custom loaders or APP_DIRS mistakes, reduce expensive template work, use fragment caching, and measure template time per view.

Problem statement

Profiling a slow page shows a surprising share of time in template rendering, not the database. Sometimes the cause is that templates are parsed from disk on every request because the cached loader is disabled by a custom TEMPLATES setting. More often, templates trigger database queries through lazy relationships, call expensive methods in loops, or render large includes repeatedly. The cached loader fixes parsing; the rest needs changes in views and templates.

Template rendering costLoad templatedisk or cacheCompileonce, then cachedRendercontext, loopsHidden querieslazy relationsThe cached loader removes parsing cost; queries in templates need view fixes.
  1. With DEBUG = False and no explicit loaders, Django already wraps loaders in the cached loader (Django 4.1+ also caches in debug mode).
  2. If you set OPTIONS["loaders"] yourself, wrap them in django.template.loaders.cached.Loader explicitly.
  3. Move data access out of templates: select_related, prefetch_related, and precomputed values in the view.
  4. Cache expensive fragments with {% cache %} and avoid heavy custom tags in tight loops.
  5. Measure template time separately (Django Debug Toolbar, APM template spans) before and after.

Step-by-step solution

1. Check the loader configuration

TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"],
    "APP_DIRS": True,
    "OPTIONS": {"context_processors": [...]},
}]

With this standard setup, Django uses the cached loader automatically. Confirm in a shell:

from django.template import engines
print(engines["django"].engine.template_loaders)

You should see a cached.Loader wrapping the filesystem and app directories loaders.

2. Fix explicit loaders

Custom loader lists disable the automatic wrapping, and APP_DIRS must be False when loaders is set:

TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"],
    "APP_DIRS": False,
    "OPTIONS": {
        "context_processors": [...],
        "loaders": [
            ("django.template.loaders.cached.Loader", [
                "django.template.loaders.filesystem.Loader",
                "django.template.loaders.app_directories.Loader",
            ]),
        ],
    },
}]

Without the cached loader, every render() reads and compiles the template and all its includes and parents from disk.

rendering time by cause (illustrative)parsing without cache90 mslazy queries in loops320 msheavy custom tags60 msafter fixes25 ms0 ms200 ms400 msQueries hidden in templates usually cost more than parsingdjango-deployment.com
Measure your own pages; the mix varies a lot between projects.

3. Remove queries from templates

{% for order in orders %}
  {{ order.customer.name }} – {{ order.items.count }} items
{% endfor %}

This runs two queries per order. Fetch everything in the view:

from django.db.models import Count
orders = (Order.objects.select_related("customer")
          .annotate(item_count=Count("items"))
          .order_by("-created_at")[:50])
{{ order.customer.name }} – {{ order.item_count }} items

Templates should display data, not fetch it.

4. Cache expensive fragments

Navigation menus, footers, and sidebars often render the same output for everyone:

{% load cache %}
{% cache 600 sidebar request.LANGUAGE_CODE %}
  {% include "partials/sidebar.html" %}
{% endcache %}

Include every variable that changes the output in the cache key. See Django Template Fragment Caching.

5. Measure

In development, the Debug Toolbar's templates and SQL panels show which templates render and which queries they trigger. In production, APM tools (Sentry, Datadog, New Relic) show template render spans per view. Compare p95 view time before and after each change.

before blaming the template engineCached loader activecheck template_loadersNo queries in loopsselect/prefetch in viewCounts via annotatenot .count in templatesFragments cachedmenus, sidebarsTemplate time measuredAPM or toolbarMost template slowness is data access in disguisedjango-deployment.com
Switching template engines rarely helps if queries are the real cost.

Explanation

Django compiles each template into a tree of nodes before rendering. The cached loader keeps compiled templates in memory per process, so later renders skip reading files and parsing. That saving is real but modest per request. Rendering also evaluates variables, which can trigger lazy ORM queries, call methods, and run custom tags; those costs scale with loop sizes and are usually the bigger problem. Fixing them happens in views (prefetching, annotations) and with output caching.

Edge cases / notes

  • Template changes in production. The cached loader keeps templates in memory until the process restarts; deploys restart workers, so this is rarely an issue.
  • Jinja2. Jinja2 has its own bytecode cache and auto-reload settings; the same query advice applies.
  • Very large templates. Splitting into includes does not slow things down with the cached loader, but deeply nested includes in loops add overhead per iteration.

Next steps

FAQ

Is the cached template loader enabled by default?

Yes, when you do not define loaders yourself. Since Django 4.1 it is used in debug mode too.

Why is my site slow even with the cached loader?

Usually templates trigger database queries or call expensive methods. Prefetch data in the view.

Do I need to restart after changing templates?

With the cached loader, yes. Deploys restart workers, which reloads templates.

Should I switch to Jinja2 for speed?

Only after removing queries and heavy work from templates. Engine speed is rarely the bottleneck.

Keep going