Payload CMS и Next.js 16: как кешировать данные и не показывать устаревший контент
Payload CMS позволяет получать данные через Local API без дополнительного HTTP-запроса, но на большом проекте одного этого недостаточно. Как только появляется кеширование Next.js, возникает более сложная задача: как понять, какие страницы нужно обновить после изменения документа, и как синхронизировать кеш между несколькими экземплярами приложения.
На небольшом сайте проблема почти незаметна. Серверный компонент запрашивает нужный документ через Payload:
const posts = await payload.find({
collection: 'posts',
})Запрос выполняется непосредственно на сервере, без REST API. Данные свежие, архитектура простая.
Но по мере роста проекта одни и те же запросы начинают выполняться сотни и тысячи раз. Появляется желание добавить кеширование. В Next.js 16 для этого есть Cache Components, директива use cache, функция cacheTag и механизмы ревалидации.
И здесь начинается самое интересное.
Разборы разработчиков Payload и Next.js — например, обсуждение в репозитории Next.js про поведение revalidateTag с профилем max — показывают, что основная сложность заключается не в том, как положить результат запроса в кеш, а в том, как гарантированно удалить его оттуда в нужный момент.
Простой вариант: кешируем запрос Payload
Упрощённо функция получения страницы может выглядеть так:
import { cacheTag } from 'next/cache'
export async function getPage(slug: string) {
'use cache'
cacheTag(`page:${slug}`)
return payload.find({
collection: 'pages',
where: {
slug: {
equals: slug,
},
},
})
}Теперь одинаковые обращения не требуют каждый раз выполнять запрос к базе.
После изменения страницы в Payload можно вызвать ревалидацию соответствующего тега — в хуке коллекции:
import { revalidateTag } from 'next/cache'
export const Pages: CollectionConfig = {
slug: 'pages',
hooks: {
afterChange: [
({ doc }) => {
revalidateTag(`page:${doc.slug}`, 'max')
},
],
},
}Для простой структуры этого может быть достаточно.
Страница почти никогда не зависит от одного документа
Проблемы начинаются, когда страница состоит не только из собственного документа.
Представим главную страницу интернет-магазина.
В Payload хранится документ home, но внутри него есть блок с популярными товарами. Товар содержит категорию. Категория — изображение. Отдельно существуют глобальные настройки сайта и меню.
Фактически результат рендеринга зависит от целого графа данных:
Главная
├── Home
├── Products
│ ├── Product A
│ │ └── Category
│ └── Product B
│ └── Category
├── Header
└── Site SettingsЕсли редактор изменил Home, всё просто: сработал его afterChange.
А что произойдёт, если он изменит название товара?
Документ Home при этом не менялся. Его afterChange не вызывается. Но HTML главной страницы уже устарел.
Именно эта проблема всплывает на реальных проектах: кеширование приходится строить не вокруг отдельных документов, а вокруг зависимостей между ними.
Теги удобнее привязывать к данным
Вместо одного тега страницы можно присвоить результату несколько тегов:
cacheTag(
'page:home',
'collection:products',
'global:header',
)После изменения товара:
revalidateTag('collection:products', 'max')будут инвалидированы все закешированные результаты, зависящие от этой коллекции.
Это грубый, но достаточно надёжный вариант.
Можно сделать систему точнее:
product:123
category:5
global:header
page:homeТогда изменение товара 123 не требует сбрасывать кеш всех страниц, использующих любые товары.
Но вместе с точностью быстро растёт сложность.
Приложению фактически приходится знать граф зависимостей контента.
Payload сам не знает, где используется документ
Предположим, товар 123 отображается:
- на собственной странице;
- на главной;
- в категории;
- в блоке рекомендаций другой статьи;
- в меню;
- в подборке «Популярное».
Хук товара знает, что товар изменился. Но он не обязательно знает все места, где результат его запроса был использован.
Можно начать обходить relationships и искать обратные зависимости, однако на сложной схеме это постепенно превращается в отдельную систему управления зависимостями.
Поэтому на практике часто используют более широкие теги. Например:
afterChange: [
() => {
revalidateTag('products', 'max')
},
]Да, после изменения одного товара будет инвалидировано больше кеша, чем строго необходимо.
Зато схема остаётся предсказуемой.
И здесь обычно приходится выбирать между эффективностью кеша и сложностью его инвалидации.
revalidateTag() не всегда означает «старых данных больше нет»
Есть ещё одна деталь Next.js, которую легко пропустить.
При вызове с профилем:
revalidateTag('products', 'max')Next.js применяет семантику stale-while-revalidate. Закешированные данные помечаются устаревшими, следующий запрос запускает ревалидацию и получает старое значение, пока обновление выполняется в фоне.
Это нормальное поведение механизма, а не обязательно ошибка Payload или Next.js.
Если вызвать revalidateTag без второго аргумента, данные истекают сразу: следующему запросу придётся дождаться обновления, и устаревший контент не будет отдан никому.
Для блога задержка обычно несущественна. Для цены товара, остатка на складе или данных личного кабинета требования могут быть совсем другими.
Поэтому сам факт вызова revalidateTag() ещё не гарантирует строгую консистентность данных — режим нужно выбирать осознанно.
Для сценариев «читай свои записи» в Server Action у Next.js есть и более строгая функция updateTag, но из Payload-хуков доступен только revalidateTag.
Несколько серверов добавляют ещё один уровень сложности
Допустим, приложение запущено в трёх экземплярах:
Load Balancer
/ | \
Node 1 Node 2 Node 3Редактор изменяет страницу. Запрос попадает на Node 1, Payload выполняет хук и инвалидирует кеш.
Но пользователи могут открыть сайт через Node 2.
Если состояние кеша и тегов хранится локально, второй экземпляр приложения может ничего не знать об изменении.
Для одного процесса всё выглядит правильно, а после горизонтального масштабирования появляются периодически устаревающие страницы.
Для self-hosted Next.js эту проблему необходимо учитывать отдельно. В документации Next.js предусмотрена настройка cacheHandlers — общий обработчик кеша, чтобы несколько экземпляров приложения использовали согласованное состояние.
И здесь естественным кандидатом становится Redis.
Где появляется Redis
Один из практических вариантов, который обсуждают разработчики Payload, — вообще вынести кеширование данных Local API из Next.js в Redis:
Next.js
│
▼
Redis
│ cache miss
▼
Payload Local API
│
▼
PostgreSQLНесколько экземпляров приложения обращаются к одному Redis:
Node 1 ─┐
Node 2 ─┼── Redis ── Payload / PostgreSQL
Node 3 ─┘Теперь инвалидировать кеш можно централизованно.
Payload выполняет afterChange, удаляет соответствующие ключи из Redis, и все серверы при следующем запросе видят одинаковое состояние.
Но Redis не решает главную логическую проблему автоматически.
Всё равно необходимо определить, какие ключи удалить после изменения конкретного документа.
Он решает другую задачу: предоставляет общее хранилище кеша для нескольких процессов.
Не всё вообще стоит кешировать
Особенно осторожно стоит относиться к данным авторизованных пользователей.
Например:
payload.find({
collection: 'orders',
user,
})Результат зависит уже не только от запроса, но и от текущего пользователя и его прав.
Ошибочная стратегия кеширования здесь может привести не просто к устаревшим данным, а к выдаче данных другого пользователя.
Для публичного каталога агрессивное кеширование обычно оправдано.
Для личного кабинета, заказов, балансов и данных с Access Control часто разумнее оставить непосредственный запрос к Payload либо проектировать кеш с обязательным включением идентификатора пользователя и контекста доступа в ключ.
Local API при этом остаётся достаточно быстрым именно потому, что ему не требуется дополнительный HTTP-запрос.
Кеш может пережить деплой
Есть ещё одна неожиданная ситуация, описанная разработчиками: закешированные данные могут сохраняться между развёртываниями приложения.
Получается неприятный сценарий:
версия приложения A
↓
cache
↓
деплой версии B
↓
используется старый cacheЕсли новая версия приложения предполагает другую структуру данных или другую логику формирования результата, старый кеш уже некорректен.
Один из простых способов защиты — добавить идентификатор версии приложения или развёртывания в ключ:
const CACHE_VERSION = process.env.DEPLOYMENT_IDи использовать его при формировании кеша:
prod-2026-10-25:product:123После нового деплоя приложение автоматически начинает работать с другим пространством ключей.
Старые значения затем можно удалить по TTL или отдельной фоновой задачей.
Какую стратегию выбрать для Payload
Универсальной схемы здесь нет, но для большинства проектов можно двигаться от простого к сложному.
Для небольшого сайта разумно сначала использовать встроенное кеширование Next.js и несколько достаточно широких тегов:
posts
products
categories
navigation
settingsНе нужно сразу строить идеальный граф зависимостей для каждого документа. Потерять часть эффективности кеша обычно дешевле, чем получить систему инвалидации, которую невозможно проверить.
Если приложение запускается в нескольких экземплярах, состояние кеша необходимо сделать общим либо использовать внешний кеш.
А данные, зависящие от пользователя и Access Control, стоит рассматривать отдельно от публичного контента.
Redis имеет смысл добавлять тогда, когда появляется конкретная причина: несколько серверов, дорогие повторяющиеся запросы Local API, необходимость общего кеша или более строгий контроль над его жизненным циклом.
Главное — проектировать инвалидацию раньше кеширования
Кешировать запрос Payload технически несложно. Сложность появляется через несколько недель, когда выясняется, что изменение категории должно обновить карточки товаров, изменение товара — главную страницу, а изменение глобальной настройки — половину сайта.
Поэтому перед добавлением кеша полезнее сначала ответить на другой вопрос:
какие данные делают этот результат актуальным и какое изменение должно его инвалидировать?
Если на него нет ответа, кеш почти неизбежно однажды покажет пользователю устаревший контент.
Для Payload это особенно актуально из-за его сильной стороны — возможности строить сложные структуры из relationships, blocks, globals и вложенных документов. Чем богаче становится модель контента, тем меньше страница похожа на результат одного запроса.
И тем важнее становится не скорость записи данных в кеш, а правильно спроектированный способ их оттуда удалить.
Источники
Дискуссия в Next.js о том, что делает revalidateTag с профилем max: https://github.com/vercel/next.js/discussions/91785
Документация revalidateTag — режимы с профилем и без: https://nextjs.org/docs/app/api-reference/functions/revalidateTag
Документация updateTag: https://nextjs.org/docs/app/api-reference/functions/updateTag
Настройка общих cacheHandlers для нескольких экземпляров: https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheHandlers
Обсуждение Redis-кеширования в Payload: https://github.com/payloadcms/payload/discussions/9909