Как самостоятельно разместить ваше Next.js-приложение

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

🎥 Видео: Подробнее о самостоятельном размещении Next.js → YouTube (45 минут).

Оптимизация изображений

Оптимизация изображений через next/image работает при самостоятельном размещении без дополнительной конфигурации при развёртывании с помощью next start. Если вы предпочитаете использовать отдельный сервис для оптимизации изображений, вы можете настроить загрузчик изображений.

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

Полезно знать:

Middleware

Middleware работает при самостоятельном размещении без дополнительной конфигурации при развёртывании с помощью next start. Поскольку для работы middleware требуется доступ к входящему запросу, она не поддерживается при использовании статического экспорта.

Middleware использует Edge runtime, подмножество всех доступных API Node.js, чтобы обеспечить низкую задержку, так как может выполняться перед каждым маршрутом или ресурсом в вашем приложении. Если это не требуется, вы можете использовать полный Node.js runtime для выполнения middleware.

Если вам нужно добавить логику (или использовать внешний пакет), требующую всех API Node.js, вы можете перенести эту логику в layout как Server Component. Например, проверка заголовков и перенаправление. Вы также можете использовать заголовки, куки или параметры запроса для перенаправления или перезаписи через next.config.js. Если это не подходит, вы можете использовать custom server.

Переменные окружения

Next.js поддерживает переменные окружения как во время сборки, так и во время выполнения.

По умолчанию переменные окружения доступны только на сервере. Чтобы сделать переменную окружения доступной в браузере, она должна иметь префикс NEXT_PUBLIC_. Однако эти публичные переменные будут встроены в JavaScript-бандл во время next build.

Для чтения переменных окружения во время выполнения рекомендуется использовать getServerSideProps или постепенно переходить на App Router.

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

Полезно знать:

  • Вы можете выполнять код при запуске сервера с помощью функции register.
  • Мы не рекомендуем использовать опцию runtimeConfig, так как она не работает с режимом standalone output. Вместо этого рекомендуем постепенно переходить на App Router.

Кэширование и ISR

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

Кэширование и ревалидация страниц (с помощью Incremental Static Regeneration) используют общий кэш. По умолчанию этот кэш хранится в файловой системе (на диске) вашего сервера Next.js. Это работает автоматически при самостоятельном размещении как с Pages Router, так и с App Router.

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

Автоматическое кэширование

  • Next.js устанавливает заголовок Cache-Control в значение public, max-age=31536000, immutable для действительно неизменяемых ресурсов. Это нельзя переопределить. Эти неизменяемые файлы содержат SHA-хэш в имени файла, поэтому их можно безопасно кэшировать бессрочно. Например, Static Image Imports. Вы можете настроить TTL для изображений.
  • Incremental Static Regeneration (ISR) устанавливает заголовок Cache-Control в значение s-maxage: <revalidate in getStaticProps>, stale-while-revalidate. Время ревалидации определяется в вашей функции getStaticProps в секундах. Если вы установите revalidate: false, продолжительность кэширования по умолчанию составит один год.
  • Динамически рендеренные страницы устанавливают заголовок Cache-Control в значение private, no-cache, no-store, max-age=0, must-revalidate, чтобы предотвратить кэширование пользовательских данных. Это применяется как к App Router, так и к Pages Router. Это также включает Draft Mode.

Статические ресурсы

Если вы хотите размещать статические ресурсы на другом домене или CDN, вы можете использовать конфигурацию assetPrefix в next.config.js. Next.js будет использовать этот префикс при загрузке JavaScript или CSS файлов. Разделение ресурсов на другой домен имеет недостаток в виде дополнительного времени, затрачиваемого на разрешение DNS и TLS.

Подробнее о assetPrefix.

Настройка кэширования

По умолчанию сгенерированные кэшированные ресурсы хранятся в памяти (по умолчанию 50 МБ) и на диске. Если вы размещаете Next.js с помощью платформы оркестрации контейнеров, такой как Kubernetes, каждый pod будет иметь свою копию кэша. Чтобы предотвратить отображение устаревших данных, так как кэш по умолчанию не является общим между pod, вы можете настроить обработчик кэша Next.js и отключить кэширование в памяти.

Чтобы настроить расположение кэша ISR/Data Cache при самостоятельном размещении, вы можете настроить пользовательский обработчик в файле next.config.js:

next.config.js
module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0, // отключить кэширование в памяти по умолчанию
}

Затем создайте cache-handler.js в корне вашего проекта, например:

cache-handler.js
const cache = new Map()

module.exports = class CacheHandler {
  constructor(options) {
    this.options = options
  }

  async get(key) {
    // Это может храниться где угодно, например в долговременном хранилище
    return cache.get(key)
  }

  async set(key, data, ctx) {
    // Это может храниться где угодно, например в долговременном хранилище
    cache.set(key, {
      value: data,
      lastModified: Date.now(),
      tags: ctx.tags,
    })
  }

  async revalidateTag(tags) {
    // tags - это строка или массив строк
    tags = [tags].flat()
    // Перебираем все записи в кэше
    for (let [key, value] of cache) {
      // Если теги значения включают указанный тег, удаляем эту запись
      if (value.tags.some((tag) => tags.includes(tag))) {
        cache.delete(key)
      }
    }
  }

  // Если вы хотите иметь временный кэш в памяти для одного запроса, который сбрасывается
  // перед следующим запросом, вы можете использовать этот метод
  resetRequestCache() {}
}

Использование пользовательского обработчика кэша позволит вам обеспечить согласованность между всеми pod, на которых размещено ваше приложение Next.js. Например, вы можете сохранять кэшированные значения где угодно, например в Redis или AWS S3.

Полезно знать:

  • revalidatePath - это удобный слой поверх тегов кэша. Вызов revalidatePath вызовет функцию revalidateTag с особым тегом по умолчанию для указанной страницы.

Кэш сборки

Next.js генерирует ID во время next build для идентификации версии вашего приложения. Одна и та же сборка должна использоваться и запускаться в нескольких контейнерах.

Если вы пересобираете для каждого этапа вашего окружения, вам нужно сгенерировать постоянный ID сборки для использования между контейнерами. Используйте команду generateBuildId в next.config.js:

next.config.js
module.exports = {
  generateBuildId: async () => {
    // Это может быть что угодно, например последний git-хэш
    return process.env.GIT_HASH
  },
}

Расхождение версий

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

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

Ручное плавное завершение работы

При самостоятельном размещении вы можете захотеть выполнить код при завершении работы сервера по сигналам SIGTERM или SIGINT.

Вы можете установить переменную окружения NEXT_MANUAL_SIG_HANDLE в true и затем зарегистрировать обработчик для этого сигнала в файле _document.js. Вам нужно будет зарегистрировать переменную окружения непосредственно в скрипте package.json, а не в файле .env.

Полезно знать: Ручная обработка сигналов недоступна в next dev.

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "NEXT_MANUAL_SIG_HANDLE=true next start"
  }
}
pages/_document.js
if (process.env.NEXT_MANUAL_SIG_HANDLE) {
  process.on('SIGTERM', () => {
    console.log('Получен SIGTERM: очистка')
    process.exit(0)
  })
  process.on('SIGINT', () => {
    console.log('Получен SIGINT: очистка')
    process.exit(0)
  })
}