Panel Caching
dj-layouts provides per-panel caching via Django's cache framework. When a panel is cached, its HTML is stored and returned directly on subsequent requests — the panel view is not re-executed. Render queue items (scripts, styles) that the panel would have enqueued are cached alongside the HTML and replayed on cache hits.
Setup
Import the cache module and add a cache= argument to your Panel:
from dj_layouts import Layout, Panel, cache
class DefaultLayout(Layout):
template = "myapp/layout.html"
nav = Panel("myapp:nav", cache=cache.sitewide(timeout=3600))
That's all. The first request renders the panel and writes to cache. Subsequent requests serve the stored HTML without calling the panel view.
Cache strategies
cache.sitewide(timeout, *, backend="default")
One cache entry shared by all users and all paths. Best for panels whose output never varies per-user or per-URL — site navigation, global footers, promotional banners.
nav = Panel("myapp:nav", cache=cache.sitewide(timeout=3600))
cache.per_user(timeout, *, backend="default")
Separate cache entries per authenticated user. The cache key includes user.pk. Anonymous users (where user.is_authenticated is False) all share a single entry keyed as "anonymous".
account_nav = Panel("myapp:account_nav", cache=cache.per_user(timeout=900))
!!! warning "All anonymous users share one cache entry"
With cache.per_user(), every unauthenticated visitor shares the same cached output. If anonymous panel output can vary by any request attribute (cookie, query parameter, session value), use cache.custom() with an explicit key_func instead.
cache.per_path(timeout, *, backend="default")
Separate cache entries per request path (request.path). Useful for panels that render differently on different pages but are the same for all users on that page.
breadcrumbs = Panel("myapp:breadcrumbs", cache=cache.per_path(timeout=300))
cache.per_user_per_path(timeout, *, backend="default")
Combines user identity and request path. Each user+path combination gets its own entry. Suitable for user-specific, page-sensitive widgets.
sidebar = Panel("myapp:sidebar", cache=cache.per_user_per_path(timeout=300))
cache.per_session(timeout, *, backend="default")
Separate cache entries per Django session key (request.session.session_key). When the session key is absent the key component falls back to "no-session".
cart_summary = Panel("myapp:cart_summary", cache=cache.per_session(timeout=60))
cache.custom(key_func, timeout, *, backend="default")
Full control over the cache key. key_func receives the request and must return a string. The returned string is appended to the base key:
def vary_by_currency(request):
return request.session.get("currency", "USD")
price_panel = Panel("myapp:price", cache=cache.custom(key_func=vary_by_currency, timeout=600))
If key_func returns an empty string the entry is sitewide (same as cache.sitewide()).
Cache key format
Keys follow this format:
layouts:panel:{panel_name} # sitewide (no vary)
layouts:panel:{panel_name}:{vary} # all other strategies
For example:
| Strategy | Panel name | Vary | Key |
|---|---|---|---|
sitewide |
nav |
— | layouts:panel:nav |
per_user |
nav |
user pk 42 |
layouts:panel:nav:42 |
per_user |
nav |
anonymous | layouts:panel:nav:anonymous |
per_path |
nav |
/about/ |
layouts:panel:nav:/about/ |
per_session |
cart |
abc123 |
layouts:panel:cart:abc123 |
custom |
price |
GBP |
layouts:panel:price:GBP |
Render queues and caching
Render queue items (scripts, styles, arbitrary queue items) added by a panel view are stored in the cache alongside the HTML. On a cache hit these items are replayed into the current request's queues — so {% renderscripts %} and {% renderstyles %} still produce correct output.
class DefaultLayout(Layout):
template = "myapp/layout.html"
scripts = ScriptQueue() # must be declared for enqueuing to work
# nav calls add_script(request, "/js/nav.js") — stored in cache on first hit
nav = Panel("myapp:nav", cache=cache.sitewide(timeout=3600))
Deduplication still applies on replay: the same script or style URL is never emitted twice in a single response.
Using a different cache backend
All strategy functions accept an optional backend= argument naming a key in settings.CACHES:
CACHES = {
"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
"redis": {"BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1"},
}
nav = Panel("myapp:nav", cache=cache.sitewide(timeout=3600, backend="redis"))
You can also change the global default backend with CACHE_BACKEND in your DJ_LAYOUTS settings dict:
# settings.py
DJ_LAYOUTS = {"CACHE_BACKEND": "redis"}
Disabling caching globally
Set CACHE_ENABLED: False in DJ_LAYOUTS to disable all panel caching regardless of cache= arguments:
# settings/local.py
DJ_LAYOUTS = {"CACHE_ENABLED": False}
This is useful in development and testing where you want deterministic, uncached rendering.
stale_ttl and refresh_func
These arguments are accepted for API stability but are not yet implemented. They are silently ignored:
# Accepted, but no stale-while-revalidate behaviour in the current version:
cache.sitewide(timeout=3600, stale_ttl=7200, refresh_func=my_refresh_fn)
Do not rely on stale-while-revalidate semantics in the current release.
Gotchas
Panel errors are not cached
If a panel raises an exception the error result is not written to cache. The panel will be re-executed on the next request, which gives a genuine cache miss (and another chance to succeed).
Panels that set response headers or cookies
Panel views run on cloned requests and their HttpResponse objects are discarded — only the response body is used. If a panel sets cookies or headers via response.cookies or response.headers, those are silently dropped. Do not rely on panel views to set response-level headers. Use middleware or the main view for that.
Cache invalidation
dj-layouts does not provide automatic cache invalidation. Use Django's cache API directly to delete entries when your data changes:
from django.core.cache import caches
# Bust the sitewide nav cache:
caches["default"].delete("layouts:panel:nav")
# Bust user 42's account nav:
caches["default"].delete("layouts:panel:account_nav:42")
For programmatic invalidation consider wrapping this in a post_save signal on the relevant model.
Per-session panels and session creation
cache.per_session() reads request.session.session_key. If the session has not been saved yet (e.g. anonymous user with no session data), the key may be None. In this case the key component falls back to "no-session" and all such requests share one entry.
Anonymous users with per_user
All anonymous users share the "anonymous" cache entry. If your anonymous panel output varies by anything other than authentication status — for example, a preferred language stored in a cookie — use cache.custom():
def vary_by_lang(request):
return request.COOKIES.get("lang", "en")
nav = Panel("myapp:nav", cache=cache.custom(key_func=vary_by_lang, timeout=3600))