Кеширование в Django: страницы, фрагменты и запросы к базе
Три уровня кеша в Django и цена каждого: когда хватает кеша фрагмента, когда нужна вся страница и почему кеш QuerySet чаще вредит, чем помогает.

У Django из коробки три разных механизма кеширования, и путать их дорого. Кеш всей страницы отдаёт байты за микросекунды, но ломает персонализацию. Кеш фрагмента экономит рендеринг тяжёлого куска шаблона, но не спасает от лишних запросов к базе. А кеш на уровне запросов люди чаще всего представляют себе неправильно — и получают устаревшие данные там, где ждали ускорения. Разберём, что каждый из трёх делает на самом деле, чем платите и когда что выбирать. Ориентир — Django 5.x, но всё описанное работает и в 4.2 LTS.
Что кешируем и на каком уровне
Прежде чем трогать декораторы, определитесь, что именно медленно. Медленный рендеринг шаблона, тяжёлый SQL и повторяющийся HTTP-ответ лечатся разными инструментами. Наложить кеш страницы на view, которое тормозит из-за N+1 запросов, — значит спрятать проблему для первого пользователя и оставить её всем остальным при промахе кеша.
Django предлагает четыре подхода, из которых три — про уровни, а четвёртый — про бэкенд хранения.
| Уровень | Что экономит | Чем платите |
|---|---|---|
| Вся страница (per-view / per-site) | Рендеринг + все запросы к БД для этого URL | Персонализация ломается, инвалидация грубая |
Фрагмент шаблона ({% cache %}) | Рендеринг куска шаблона | Запросы к БД внутри фрагмента всё равно уйдут, если ORM их ленит |
Низкоуровневый API (cache.get/set) | Ровно то, что вы решили закешировать | Инвалидацию пишете руками |
Кеш всей страницы — это ускорение анонимного трафика. Как только в ответе появляется имя пользователя или корзина, вы уже не на этом уровне.
Настройка бэкенда: без него всё пишется в никуда
По умолчанию кеш работает через LocMemCache — процессная память одного воркера. Для одного процесса на dev-машине этого хватает, но в проде с несколькими воркерами Gunicorn у каждого будет свой кеш, и попадания станут случайными. Для боевого продакшена — общий бэкенд, чаще всего Redis (поддержка встроена начиная с Django 4.0):
# settings.py, Django 5.x
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
# KEY_PREFIX страхует от коллизий, если Redis делят проекты
"KEY_PREFIX": "shop",
"TIMEOUT": 300, # секунды по умолчанию, если не указать явно
}
}
TIMEOUT: None означает «хранить вечно», а 0 — «не кешировать». Это частый источник путаницы: ноль не равен бесконечности.
Подход 1: кеш всей страницы
Самый грубый и самый быстрый. Декоратор cache_page оборачивает view и хранит готовый HTTP-ответ:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # 15 минут
def article_list(request):
articles = Article.objects.filter(published=True)
return render(request, "articles/list.html", {"articles": articles})
Ключ кеша Django строит из полного URL, включая query-параметры, и из заголовков, перечисленных в Vary. Отсюда два подводных камня:
- Персонализация. Если внутри шаблона есть
{{ request.user }}, первый залогиненный пользователь запишет свою версию страницы в кеш, и её увидят все. Для страниц с авторизацией либо не кешируйте их per-view, либо добавляйтеVary: Cookieчерез@vary_on_cookie— но тогда кеш почти не переиспользуется. - Query-параметры.
?utm_source=...плодит отдельные записи в кеше. Одна и та же страница с сотней разных меток — это сто копий в Redis.
Когда брать: анонимные лендинги, страницы каталога без учёта пользователя, RSS. Не брать: личный кабинет, корзина, всё, что зависит от сессии.
Подход 2: кеш фрагмента шаблона
Когда страница персональна в целом, но содержит тяжёлый общий кусок — меню категорий, блок «популярное», футер с большим количеством ссылок — кешируют именно фрагмент. Тег {% cache %} из библиотеки cache:
{% load cache %}
{% cache 600 sidebar_categories %}
<ul>
{% for category in categories %}
<li>{{ category.name }} ({{ category.product_count }})</li>
{% endfor %}
</ul>
{% endcache %}
Первый аргумент — время жизни в секундах, второй — имя фрагмента. Если контент зависит от переменной, её добавляют в ключ третьим и далее аргументом:
{% cache 600 user_widget request.user.username %}
<div>Привет, {{ request.user.username }}</div>
{% endcache %}
Главная ловушка: {% cache %} экономит только рендеринг HTML. Если categories — это ленивый QuerySet, который дёргает базу при итерации в шаблоне, то при промахе кеша запрос всё равно уйдёт. Кеш фрагмента не заменяет кеш данных.
Подход 3: низкоуровневый API и то, чем не является «кеш запросов»
Здесь живёт главное недоразумение. В Django нет встроенного прозрачного кеширования результатов ORM-запросов между HTTP-запросами. QuerySet кеширует результат только внутри своего объекта в пределах одного запроса — повторная итерация по тому же QuerySet базу не дёргает, но новый QuerySet в следующем HTTP-запросе пойдёт в БД заново.
Чтобы переиспользовать результат запроса между запросами пользователей, вы кешируете данные сами через низкоуровневый API:
from django.core.cache import cache
def get_active_categories():
key = "active_categories"
categories = cache.get(key)
if categories is None:
# list() форсирует выполнение QuerySet — в кеш кладём данные,
# а не ленивый объект
categories = list(
Category.objects.filter(is_active=True).values("id", "name")
)
cache.set(key, categories, 60 * 30) # 30 минут
return categories
Два момента, которые ломают такой код в проде:
- Кешировать QuerySet, а не список. Если положить в кеш сам объект
QuerySet, он засериализуется вместе с ленивым состоянием и при распаковке может снова пойти в базу или упасть. Форсируйте выполнение черезlist()или.values(). - Инвалидация. Изменили категорию — кеш об этом не узнает и будет отдавать старое ещё 30 минут. Либо коротким TTL миритесь с задержкой, либо чистите ключ в сигнале
post_save/post_deleteмодели. Инвалидация по событиям — отдельная большая тема, здесь только отмечаем, что молчаливый TTL и явная очистка — два разных решения с разной ценой.
Когда что выбирать
- Страница одинакова для всех и меняется редко —
cache_page. Максимум переиспользования, минимум кода. - Страница персональна, но есть общий тяжёлый блок —
{% cache %}на этот блок. Не забудьте отдельно закешировать данные, если запрос под блоком дорогой. - Дорогой запрос или вычисление, которое переиспользуется во многих местах — низкоуровневый
cache.get/setс явным ключом и продуманной инвалидацией.
Комбинировать уровни нормально: cache_page на анонимную версию каталога плюс cache.set для справочника категорий, который нужен и в других view.
Что ломается чаще всего
- LocMemCache в проде. Несколько воркеров — несколько несогласованных кешей. Симптом: страница то свежая, то старая при перезагрузке. Лечится переходом на Redis или Memcached.
- Утечка персональных данных через
cache_page. Логин попал в общий кеш. Проверяйте, что кешируете только анонимные ответы. - Забытая инвалидация. Данные обновились в админке, а сайт отдаёт старое. Либо короткий TTL, либо чистка ключа по сигналу.
- Раздувание кеша query-параметрами. UTM-метки создают тысячи записей. Отсекайте лишние параметры до кеширования или нормализуйте URL.
Что настроить дальше
Начните с замеров, а не с декораторов: подключите django-debug-toolbar и посмотрите, сколько запросов и времени уходит на конкретный URL. Часто оказывается, что проблема не в отсутствии кеша, а в N+1 — и лечится select_related/prefetch_related без единой строки кеша. Кеш накладывают на то, что уже оптимизировано в SQL, иначе вы кешируете плохой запрос. После внедрения смотрите в проде на hit rate Redis: низкий процент попаданий означает, что ключи слишком гранулярны и кеш почти не работает.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Django documentation — Django’s cache framework
Частые вопросы
Есть ли в Django автоматический кеш результатов ORM-запросов?
У меня Django 4.2, а не 5.x — примеры подойдут?
Кеш не обновляется после изменения данных в админке — что делать?
Можно ли ставить cache_page на страницы с авторизацией?
LocMemCache или Redis для продакшена?
Что положить в кеш — QuerySet или список?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.