Стратегии кэширования в Next.js

Next.js это увлекательный фреймворк, который предлагает надежную поддержку для написания сложных приложений React с такими функциями, как рендеринг на стороне сервера и статическая генерация.

Тем не менее, Next.js поведение кэширования, безусловно, является наиболее критикуемым аспектом фреймворка, и многим разработчикам не нравится, как он работает. Хотя кэширование является важным фактором для хорошего приложения React, оно может легко привести к сложным ошибкам, отладка которых может занять много времени, если их не понимать.

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

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

В этой статье мы рассмотрим четыре различных механизма кэширования, разберем их и узнаем, как эффективно ими управлять:

  • Запрос на запоминание
  • Кэш данных
  • Полный кэш маршрутов
  • Кэш маршрутизатора

Мы также рассмотрим недействительность кэша, некоторые Next.js инструменты кэширования, рекомендации и многое другое!

Next.js механизмы кэширования

Кэширование просто означает хранение извлеченных или вычисленных данных во временном хранилище для последующего доступа вместо необходимости повторной выборки или повторного вычисления каждый раз, когда это необходимо. В Next.js кэширование очень агрессивно, что означает, что оно кэширует все возможное, например, извлеченные данные, посещенные маршруты и многое другое.

Для достижения этой цели Next.js существует четыре различных механизма кэширования, которые работают на разных этапах вашего приложения React, о которых мы поговорим ниже.

Запрос на запоминание

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

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

Запоминание запросов удобно тем, что разработчикам не нужно извлекать данные из верхней части дерева, а затем передавать их вниз через props. Вы можете выбирать для извлечения данных любое место и не беспокоиться о выполнении нескольких ненужных HTTP-запросов к серверу для получения одних и тех же данных.

Однако механизм запоминания запросов работает только с собственной функцией выборки и только в том случае, если запросы в точности совпадают, т.е. имеют один и тот же URL-адрес и объект параметров. Этот механизм является функцией React, что означает, что он должен выполняться внутри компонента React, а не в серверном действии или обработчике маршрута.

Вот пример запоминания запросов в действии. В этой настройке функция GetProducts извлекает список продуктов из конечной точки API. При первом вызове данные кэшируются; последующие вызовы извлекают данные из кэша, а не выполняют дополнительные сетевые запросы:

export const getProducts = async () => {
  const res = await fetch( 'https://mystoreapi.com/products');
  const data = await res.json();
  return data;
};

import { getProducts } from '../../../lib/products';
import ProductList from '../productList/page';

const Product = async () => {
  const products = await getProducts();
  const totalProducts = products?.length;

  return (
    <div>
      <div>{`There are ${totalProducts} total number of products in my store.`}</div>
      <ProductList />
    </div>
  );
};

export default Product;

import { getProducts } from '../../../lib/products';

const ProductList = async () => {
  const products = await getProducts();
  return ( 
    <ul> 
      {products?.map(({id, title}) => (
        <li key={id}>{title}</li>
      ))} 
    </ul>
  );
}; 

export default ProductList;

В приведенных выше фрагментах кода у нас есть функция GetProducts, которая извлекает данные о продуктах из источника данных. Затем есть еще два компонента, которые используют данные о продуктах: Products и ProductsList.

Компонент Products вызывает функцию GetProducts и использует данные для отображения количества продуктов, а также существует экземпляр компонента ProductsList, который также вызывает функцию GetProducts.

При первом вызове функции GetProducts данные возвращаются и затем временно сохраняются в кэше запоминания запросов для использования в будущем.

Второй вызов функции GetProducts произошел в компоненте ProductsList, который отображается в компоненте Products. Таким образом, при втором вызове функции GetProducts не будет сетевого запроса к удаленному источнику данных; вместо этого данные будут поступать из кэша запоминания запросов.

Важно отметить, что URL-адреса в обоих вариантах использования функции GetProducts одинаковы.

Кэш данных

Кэш данных хранит все данные, которые были получены либо по определенному маршруту, либо из одного запроса на выборку. Уникальность кэша данных заключается в том, что кэшированные данные остаются там практически навсегда, если только вы не решите повторно проверить кэш, что, как помните, означает очистку кэша от старых данных и обновление его новыми.

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

Это означает, что эти данные используются для статического отображения маршрутов. При повторной проверке этих данных текущая статическая страница, отображаемая в данный момент, будет восстановлена. В этом заключается вся идея ISR (инкрементальной статической регенерации).

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

Полный кэш маршрутов

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

Концептуально, полный кэш маршрутов - это не что иное, как создание статических маршрутов и сохранение их в виде полезной нагрузки в формате HTML и RSC. Поскольку этот кэш связан с кэшем данных, он сохраняется до тех пор, пока первый не станет недействительным, т.е. данные не будут удалены.

В приведенном ниже примере мы используем полное кэширование маршрута, чтобы отобразить компонент продукта в виде статической HTML-страницы с сохраненными данными:

import Link from 'next/link';
import { getProducts } from '../../../lib/products'; 

const Product = async () => {
  const products = await getProducts();
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {products.map(({id, title}) => (
          <li key={id}>
            <Link href={`/blog/${id}`} >
              <a>{title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Product;

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

Кэш маршрутизатора

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

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

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

Кроме того, на страницах хранятся в течение 30 секунд, если динамический и пяти минут, если статический, и кэш не может быть повторно, если пользователь не сложно-перезагружает страницу или закрывается и открывается вкладка. Это самый большой недостаток кэширования Next.js .

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

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

Next.js аннулирование кэша

Аннулирование кэша очищает кэш от старых данных и обновляет его новыми данными, в то время как повторная проверка кэша проверяет, не устарели ли кэшированные данные; если это так, к исходному источнику данных отправляется новый HTTP-запрос на получение свежих данных, т.е. недействительных.

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

Стратегии для аннулирования кэша

Ниже приведены некоторые стратегии для реализации аннулирования кэша в Next.js:

Истечение срока действия на основе времени

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

import { getProducts } from '../../../lib/products';

const Page = async () => {
  const revalidate = 3600;
  const products = await getProducts();

  return (
    <div className='space-y-8'>
      {products.map(({ id, title, description }) => (
        <div key={id}>
          <h2>{title}</h2>
          <p>{description}</p>
        </div>
      ))}
    </div>
  );
};

export default Page;

В приведенном выше коде эта страница использует ISR, и HTML-код будет сгенерирован во время сборки. Но поскольку страница использует ISR, кэш данных будет аннулироваться каждые 3600 секунд, что составляет ровно час. Это гарантирует, что страница остается актуальной и синхронизированной с исходным источником данных.

Ручное кэширование или аннулирование по требованию

Это имеет смысл только в серверном действии, где вы можете аннулировать кэш при каждом вызове серверного действия. Аннулирование кэша вручную может быть выполнено либо с помощью revalidatePath, либо с помощью revalidateTag.

В приведенном ниже примере мы реализуем ручное аннулирование с помощью функции revalidatePath, таким образом гарантируя, что при следующем запросе к маршруту /users будут получены свежие данные с сервера:

'use server';

import { revalidatePath } from 'next/cache';

export const getUsers = async () => {
  const res = await fetch('https://mystoreapi.com/users');
  const data = await res.json();

  revalidatePath('/users');
  return data;
};

Директива use server вверху указывает, что функция предназначена для запуска только на сервере. Это означает, что функция является серверным действием, в котором имеют смысл функции, специфичные для сервера, такие как повторная проверка пути/тега.

Каждый раз, когда вызывается функция getUsers, она сообщает Next.js об аннулировании кэша, ответственного за маршрут /users, гарантируя, что новые данные будут доступны на странице для пользователей:

'use server';

import { revalidateTag } from 'next/cache';

export const getProducts = async () => {
  const res = await fetch('https://mystoreapi.com/products', {
    next: { tags: ['products'] },
  });
  const data = await res.json();
  return data;
};

export const updateProducts = async () => {
  revalidateTag('products');
};

В приведенном выше фрагменте кода серверное действие GetProducts извлекает данные из исходного источника данных и немедленно сообщает Next.js чтобы кэшировать его, используйте products в качестве тега данных.

Серверное действие updateProducts выполняет логику обновления, а затем сообщает Next.js о необходимости повторной проверки всех данных кэша, связанных с определенными тегами products. Эта стратегия очень полезна, когда многие страницы или компоненты зависят от одних и тех же данных.

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

В следующем разделе мы рассмотрим некоторые инструменты, которые значительно упрощают кэширование в Next.js и могут оптимизировать ваш рабочий процесс как разработчика.

Next.js инструменты кэширования

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

В этом разделе мы представим пошаговое руководство по двум инструментам кэширования Next.js.

далее-кэш-панель инструментов

В Next.js кэширование не используется в процессе разработки, только в рабочей среде. Это одна из причин, по которой с этим возникают проблемы. Панель инструментов next-cache-toolbar - отличный инструмент для кэширования, используемый в разработке для отображения информации о страницах в кэше.

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

Основные возможности next-cache-toolbar:

  • Визуальная обратная связь — легко увидеть, кэшируются ли страницы и где хранится кэш
  • Мониторинг в режиме реального времени - обеспечивает постоянную информацию о производительности кэша и использовании данных.
  • Попадание/промах в кэш — показывает, была ли страница загружена из кэша (попадание) или сгенерирована повторно (промах).

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

панель инструментов next-cache-toolbar очень полезна, особенно для просмотра страниц, созданных с использованием Next.js таких функций, как SSR или ISR. Ниже приведены пошаговые инструкции по добавлению панели инструментов next-cache-toolbar в ваш рабочий процесс Next.js.

Установить далее-кэш-панель инструментов

Добавьте панель инструментов next-cache-toolbar в свой Next.js проект. Это можно сделать с помощью npm:

npm install next-cache-toolbar

Или Пряжа:

yarn add next-cache-toolbar

Настройка следующей панели инструментов-кэширование

Панель инструментов next-cache-toolbar требует использования App Router, поскольку для эффективной интеграции и функционирования в рабочем процессе необходимы некоторые настройки Next.js. Для настройки панели инструментов next-cache-toolbar необходимо выполнить следующие действия:

Создайте файл toolbar.jsx или toolbar.tsx на панели инструментов

Этот файл важен, так как он позже загружается с задержкой, чтобы предотвратить связывание next-cache-toolbar в рабочей среде. В каталоге a`pp создайте файл toolbar.jsx или toolbar.tsx и напишите код, приведенный в приведенном ниже фрагменте:

// app/toolbar.jsx
import { NextCacheToolbar } from "next-cache-toolbar";
import "next-cache-toolbar/style.css";
const Toolbar = () => {
    return <NextCacheToolbar />;
}
export default Toolbar;

Настройте корневой файл layout.tsx или layout.jsx

После создания и настройки панели инструментов пришло время ее использовать:

export const metadata = {
  title: 'Next.js',
  description: 'Generated by Next.js',
};

// app/layout.jsx
let Toolbar: React.ComponentType = () => null;

if (process.env.NODE_ENV === "development") {
  Toolbar = dynamic(() => import("./toolbar"));
}
export default function RootLayout({ children }) {
  return (
    <html>
      <head />
      <body>
        {children}
        <Toolbar />
      </body>
    </html>
  );
}

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

Запустите программу разработки

Это самый простой шаг. Откройте свой терминал и введите npm run dev, чтобы запустить сервер разработки. Вверху вашей страницы появится панель инструментов next-cache-toolbar.

next-shared-cache

Этот инструмент, также известный как @neshca/cache-handler, представляет собой специализированный ISR/data cache API, созданный для Next.js приложений. Эта библиотека еще больше упрощает процесс кэширования, тем самым уменьшая сложность управления кэшированными данными.

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

Кроме того, в отличие от панели инструментов next-cache-toolbar, которая поддерживает только App Router, она полностью поддерживает Page и App Router, что является большим плюсом.

Основные преимущества next-shared-cache:

  • Многоразовый кэш — легко повторно используйте кэшированные данные в различных частях вашего приложения, сокращая избыточность.
  • Настраиваемое хранилище — поддержка различных систем хранения данных, таких как Redis или кэширование на основе памяти, в соответствии с потребностями вашего приложения.
  • Повышение производительности — сокращается необходимость в повторном извлечении и пересчете данных, что повышает общую производительность вашего приложения.

Следующие шаги помогут вам выполнить установку и использование, чтобы начать использовать расширенное кэширование в своих проектах Next.js.

Установка

Откройте свой терминал и перейдите в папку вашего проекта, затем выполните эту команду:

npm install @neshca/cache-handler

Конфигурация

Создайте файл дескриптора кэша с именем cache-handler.js в корневом каталоге вашего проекта. Этот файл будет содержать конфигурацию. Настройте файл с помощью приведенного ниже кода:

import { CacheHandler } from '@neshca/cache-handler';

CacheHandler.onCreation(async () => {
  const cacheStore = new MagicMap();
  const handler = {
    async get(key) {
      return await cacheStore.get(key);
    },
    async set(key, value) {
      await cacheStore.set(key, value);
    },
    async revalidateTag(tag) {
      for (const [key, { tags }] of cacheStore) {
        if (tags.includes(tag)) {
          await cacheStore.delete(key);
        }
      }
    },
    async delete(key) {
      await cacheStore.delete(key);
    },
  };
  return {
    handlers: [handler],
  };
});

export default CacheHandler;

Интегрируйтесь с Next.js

После установки и настройки конфигурации пришло время интегрировать инструмент кэширования в ваше приложение Next.js.

Чтобы использовать обработчик кэша, который должен быть активен только в рабочей среде, обновите next.config.js (у вас может быть .mjs — не беспокойтесь, это все тот же файл). Ваш next.config.js файл теперь должен выглядеть следующим образом:

const nextConfig = {
  cacheHandler:
    process.env.NODE_ENV === 'production'
      ? require.resolve('/cache-handler.mjs')
      : undefined,
  experimental: { 
    instrumentationHook: true,
  },
};
module.exports = nextConfig;

Заполните кэш предварительно отрисованными страницами

Статические страницы предварительно отображаются. На статических страницах HTML-код для маршрута генерируется во время сборки или периодически в фоновом режиме путем предварительной выборки данных в ISR.

В этом случае всегда рекомендуется заполнять кэш этого инструмента предварительно отрисованными страницами, предоставляя ему исходные данные.

Создайте файл instrumentation.js (или .ts) в корневой папке вашего проекта и заполните его приведенным ниже кодом:

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { registerInitialCache } = await import(
      '@neshca/cache-handler/instrumentation'
    );
    const CacheHandler = (await import('../cache-handler.mjs')).default;
    await registerInitialCache(CacheHandler);
  }
}

Создайте и запустите свое приложение

@neshca/обработчик кэша активен только в рабочей среде, что означает, что вам нужно создать свой проект и запустить его в рабочей среде. Чтобы предотвратить разделение операций, когда вы сначала создаете, а затем запускаете, мы можем использовать возможности скрипта.

Создайте новый скрипт в файле package.json

Создайте новое свойство в объекте scripts и назовите его как угодно. В этом фрагменте кода мы назвали свойство script prod.:

"scripts": {
   "dev": "next dev",
   "build": "next build",
   "start": "next start",
   "lint": "next lint",
   "prod": "next build && next start"
}

Теперь запустите npm run prod или npm run yourscriptname в терминале, чтобы начать работу с обработчиком кэша.

Рекомендации по Next.js кэширование

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

Защита хранилища конфиденциальных данных

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

Установите соответствующие заголовки для управления кэшем

Используйте заголовки для управления кэшем в своих ответах, чтобы определить, как долго данные должны храниться в кэше. Эти заголовки могут определять время истечения срока действия, могут ли данные храниться в общедоступном кэше (например, в CDN) и многое другое:

res.setHeader('Cache-Control', 'public, max-age=3600, stale-while-revalidate=59');

Повторная проверка часто обновляемых данных

Если ваше приложение работает с часто обновляемыми данными, установите соответствующее время повторной проверки, чтобы пользователи могли видеть свежий контент без ущерба для производительности.

Мониторинг производительности кэша

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

Распространенные проблемы с кэшированием Next.js и способы их решения

Пропуски в кэше

Сбои в работе кэша возникают, когда данные не найдены в кэше и их необходимо извлекать повторно. Это может произойти из-за неправильной настройки или истечения срока действия кэша. Используйте панель инструментов next-cache-toolbar для выявления сбоев и оптимизации времени работы кэша.

Если вы обнаружили некоторые сбои в работе кэша с помощью next-cache-toolbar, вам определенно нужно оптимизировать продолжительность работы кэша на основе этих сбоев. Вот как это сделать для двух сценариев:

getStaticProps или getServerSideProps (стратегия ISR)

Вы можете просто настроить время повторной проверки в зависимости от частоты пропусков в кэше. Если информация обновляется слишком медленно, сократите время проверки, чтобы она обновлялась быстрее, а если она обновляется слишком быстро, увеличьте время проверки, как показано ниже.:

export async function getStaticProps() { 
  const data = await fetchBlogPost();
  return {
    props: {
      data,
    },
    revalidate: 3600, // 1 hour (3600 seconds)
  };
}

Маршруты API (заголовки для управления кэшем)

Здесь просто настройте пользовательские заголовки для управления кэшированием, чтобы контролировать, как долго ответы кэшируются в браузере или CDN. Вы можете настроить свой заголовок в зависимости от количества пропусков в кэше, как показано ниже:

export default async function handler(req, res) {
  const data = await fetch('https://api.example.com/data');
  res.setHeader('Cache-Control', 'public, max-age=3600, stale-while-revalidate=59'); // Cache for 1 hour
  res.status(200).json(data);
}

Устаревшие данные

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

Избыточное кэширование

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

Вывод

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