Главная / Статьи / Понимание директивы «use cache» и процесса повторной валидации на основе тегов в Next.js 16

Понимание директивы «use cache» и процесса повторной валидации на основе тегов в Next.js 16

Узнайте, как работает директива «use cache» в Next.js 16, её сопутствующие функции повторной верификации, а также как применять кэширование с учётом тенантов в многотенантных приложениях.

1215 слов

В Next.js кэширование традиционно считалось своего рода «черным ящиком» — совокупностью настроек на уровне файлов, параметров, передаваемых в функцию fetch, и стандартных параметров фреймворка, которые достаточно сильно менялись между версиями, из-за чего даже опытные разработчики всегда оставляли открытую документацию на всякий случай. Директива "use cache", введенная в Next.js 16 в рамках более широкой концепции компонентов кэширования, меняет эту ситуацию, позволяя явно указывать поведение кэширования на уровне компонента или функции вместо того, чтобы косвенно наследовать его из стандартных параметров, которые необходимо запоминать.

Ниже приведен разбор того, что на самом деле делает эта директива, и ситуаций, когда ее использование целесообразно.

Что на самом деле делает директива

Указание "use cache" внутри функции или компонента принуждает Next.js кэшировать результаты работы этой функции. Концептуально это похоже на "use client", за исключением того, что вместо обозначения границы клиентской части кода здесь задаётся граница кэширования:

async function getDashboardStats(tenantId: string) {
  "use cache";
  const stats = await db.query.stats.findMany({ where: { tenantId } });
  return stats;
}

Результат работы функции кэшируется и привязывается к ключу на основе переданных аргументов, после чего повторно используется в последующих запросах до тех пор, пока что-то не сделает его устаревшим. Это значительное отличие от более старого подхода, при котором кэшировались отдельные запросы или целые сегменты маршрута: теперь можно кэшировать данные на любом уровне детализации, вплоть до отдельной функции.

Три вспомогательные функции

Помимо этой директивы, Cache Components предоставляют небольшой набор API для целенаправленного управления кэшированными данными, вместо простого ожидания истечения времени кэширования:

  • revalidateTag(tag) — удаляет все записи кэша, связанные с заданной тегом. Это удобно, когда одна изменение затрагивает данные, от которых зависят несколько разных функций кэша.
  • updateTag(tag) — узкая версия той же идеи, предназначенная для обновления конкретных записей кэша, связанных с одним тегом, а не всего, что находится под ним.
  • refresh() — обновляет данные кэша, применимые к текущему запросу.

Вот принцип, который делает всю систему эффективной: присваивайте каждой функции, находящейся в кэше, значимый тег — что-то вроде tenant-stats или invoice-list — и каждый раз, когда происходит изменение, влияющее на соответствующие данные, вызывайте revalidateTag с соответствующим тегом, вместо того чтобы пытаться определить временной интервал истечения, который будет слишком коротким (что сводит на нет цель кэширования) или слишком длинным (что приводит к использованию устаревших данных).

async function getInvoices(tenantId: string) {
  "use cache";
  cacheTag(`invoices-${tenantId}`);
  return db.query.invoices.findMany({ where: { tenantId } });
}
// After creating an invoice:
async function createInvoice(data: InvoiceInput) {
  await db.insert(invoices).values(data);
  revalidateTag(`invoices-${data.tenantId}`);
}

Именно это сочетание — присвоение тегов при чтении и повторная верификация при записи — представляет собой суть всей концепции в миниатюре. Почти все остальные действия, которые вы будете выполнять с этой системой, являются вариациями того же принципа.

Частые моменты путаницы

Самая распространённая ошибка, в которую попадают команды, заключается в предположении, что "use cache" является простым заменителем опции next: { revalidate } в функции fetch(). На самом деле они решают разные, хотя и связанные между собой проблемы. Опция на уровне fetch() управляет одним сетевым запросом. "use cache" хранит в кэше результат всей функции или компонента, который внутри себя может выполнять гораздо больше действий — обращаться к базе данных, выполнять вычисления, а возможно, даже вызывать саму функцию fetch. Если вам нужно лишь сохранить в кэше результат одного внешнего API-запроса, то использование кэширования на уровне fetch() обычно является более простым решением. "use cache" становится полезным тогда, когда требуется сохранить в кэше весь вычисленный результат, а не только отдельный запрос, который его генерирует.

Вторая распространённая ошибка — это полное пропускание шага добавления меток, из-за чего позже возникают проблемы, когда мутация не удаляет сохранённые данные. Без привязанной метки единственным механизмом аннулирования данных является время, что снижает ценность самой идеи использования этой модели — вы берёте на себя дополнительную сложность явного кэширования, не получая при этом контроля над процессом аннулирования.

Метки, учитывающие арендаторов, для приложений с несколькими арендаторами

Если вы работаете над системой с несколькими арендаторами, есть важный момент, который стоит отметить сразу: теги должны содержать информацию об арендаторе, а не только о типе данных, которые хранятся в кэше. Общий тег вроде invoices, используемый для всех арендаторов, означает, что аннулирование данных одного арендатора повлияет на все — это либо приводит к проблемам с корректностью (один арендатор видит устаревшие данные из-за того, что изменения другого арендатора вызвали общую перепроверку), либо к проблемам с производительностью (кэш очищается гораздо чаще, чем необходимо). Формат вроде invoices-${tenantId}, используемый в примере выше, — это не стилистический выбор; именно он отличает рабочую стратегию аннулирования данных от несработающей.

Почему выгодно правильно настроить всё с самого начала

Слой кэширования с незаметным дефектом редко выходит из строя в очевидной форме. Вместо этого это проявляется в нечетких заявках на поддержку вроде «почему на этом панели управления по-прежнему отображаются данные прошлой недели» — ошибках, которые крайне сложно отследить, поскольку некорректный механизм аннулирования кэша часто находится в месте, которым никто не занимался уже месяцами. Раннее внедрение последовательной стратегии маркировки, применяемой единообразно ко всем функциям с кэшированием в кодовой базе, является одним из тех инфраструктурных решений, которые дешево реализовать с самого начала, но значительно дороже исправлять позже.

Некоторые наборы для начала работы с панелями управления строят свой слой данных именно на таком подходе. Например, шаблон вроде шаблона панели управления Ovyqen для проектов Next.js и SaaS применяет принцип добавления тегов при чтении и повторной верификации при записи на всех этапах, включая идентификаторы арендаторов в каждый тег с самого начала, вместо того чтобы добавлять их позже после сбоев кэширования между арендаторами. Если вы рассматриваете варианты шаблонов панелей управления и проблема аннулирования кэша — это та часть вашего существующего приложения, которой никто по-настоящему не доверяет, то это веская причина начать с основы, которая уже правильно решает эту проблему.

Часто задаваемые вопросы

Следует ли мигрировать каждый вызов fetch() на "use cache"? Не обязательно — эти инструменты частично перекрываются, но не являются взаимозаменяемыми. Кэширование на уровне fetch по-прежнему подходит для простых сценариев с одним запросом. Используйте "use cache", когда вам нужно сохранить в кэше рассчитанный результат или выходные данные всего компонента.

Является ли "use cache" достаточно зрелым для производственных панелей управления? Относитесь к нему так же, как и к любому относительно новому механизму кэширования — тщательно протестируйте способы аннулирования кэша, особенно для данных с множеством арендаторов, прежде чем полагаться на него в тех функциях, которые предназначены для клиентов и где важна актуальность данных.

Что происходит, если функция из кэша остается без меток? Кэширование всё равно происходит, но теряется возможность умышленно аннулировать его в ответ на определённое событие. Вы вынуждены полагаться исключительно на временные ограничения, что редко соответствует желаемому поведению.

Явное кэширование требует больше усилий на начальном этапе, чем простое использование стандартных настроек фреймворка. Однако для данных дашборда, где отображение устаревшей информации имеет реальные последствия, эти дополнительные усилия почти всегда являются правильным решением.

Связанные материалы

  • 20 передовых шаблонов Next.js для приложений App Router высокого уровня — Изучите двадцать шаблонов высокого уровня для Next.js, охватывающих подходы с серверной инициализацией, стриминг, кэширование, маршрутизацию и оптимизацию производительности для создания более быстрых и масштабируемых приложений.
  • Как инженерия фронтенда эволюционировала от стилизации до систем масштабирования — Рассматривается переход от простых технологий HTML/CSS/JS к архитектуре компонентов, кэшированию, монорепозиториям и инструментам для отслеживания, необходимым для надежной работы с миллионами пользователей.