Все уроки
ФронтендСредний15 мин чтенияВ моём стеке

Next.js

React-фреймворк для быстрых и SEO-дружелюбных сайтов: App Router, серверный рендеринг и кэширование.

  • App Router
  • SSR / SSG / ISR
  • Caching
  • Metadata
  • Image & fonts
Шаг 1/5
CSR: сервер присылает почти пустую HTML-заготовку и большой бандл JavaScript. Браузер крутит спиннер и берётся за работу.

За 30 секунд

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

Почему я это использую

  • Я делаю на Next.js интернет-магазины, маркетинговые сайты и веб-приложения, потому что он даёт страницы с серверным рендерингом, дружелюбные к SEO, не отказываясь от компонентной модели React.
  • Я переписал большой сайт агентства со старого Pages Router на App Router, поэтому знаю обе модели и путь миграции между ними.
  • Я ценю, что функции производительности (оптимизация изображений, self-hosted шрифты, code splitting, стриминг) включены по умолчанию, а не собираются вручную в каждом проекте.
  • Я запускаю Next.js в продакшене на собственных Linux-серверах за nginx, поэтому понимаю, что нужно фреймворку вне управляемой платформы.

Как это работает

01App Router: файлы становятся маршрутами

В App Router папки внутри app/ задают сегменты URL, а специальные файлы определяют, что показывает каждый сегмент. page.tsx делает маршрут публично доступным. layout.tsx оборачивает сегмент и всех его потомков и остаётся смонтированным, пока пользователь переходит между дочерними страницами, поэтому шапки, сайдбары и состояние сохраняются. loading.tsx показывается мгновенно, пока страница стримится (это граница Suspense), error.tsx перехватывает ошибки в этом сегменте, а not-found.tsx рендерит контент для 404. Динамические сегменты используют квадратные скобки, например [slug]; группы маршрутов в круглых скобках, например (shop), упорядочивают файлы, не меняя URL. route.ts создаёт API-эндпоинт (route handler) с функциями GET или POST. Навигация использует компонент <Link>, который в фоне делает prefetch маршрутов, поэтому клики ощущаются мгновенными. Начиная с Next.js 15, params и searchParams — это промисы, которые нужно await'ить.

text
app/
  layout.tsx              // root layout: <html>, <body>, fonts
  page.tsx                // "/"
  (shop)/
    layout.tsx            // shared shop layout, URL unchanged
    products/
      page.tsx            // "/products"
      loading.tsx         // streamed skeleton
      [slug]/
        page.tsx          // "/products/blue-shoe"
        not-found.tsx
  api/
    health/route.ts       // GET /api/health
  sitemap.ts              // /sitemap.xml
  robots.ts               // /robots.txt

02Server и Client Components в Next.js

Каждый компонент в директории app по умолчанию — Server Component. Он выполняется на сервере, может быть асинхронным, может обращаться к базе данных или вызывать внутренний API с секретами и не отправляет в браузер собственного JavaScript. Когда нужна интерактивность (состояние, эффекты, обработчики событий, API браузера), создайте Client Component, поставив "use client" в начале файла. Эта директива задаёт границу: файл и всё, что он импортирует, становятся частью клиентского бандла. Поэтому опускайте "use client" как можно ниже по дереву — например, делайте клиентским только кнопку «В корзину», а не всю страницу товара. Server Components могут передавать Client Components сериализуемые props (строки, числа, простые объекты, промисы) и передавать в них отрендеренный на сервере контент как children. Для мутаций Server Actions (функции с пометкой "use server") обрабатывают отправку форм без рукописного API-маршрута.

tsx
// app/products/[slug]/page.tsx  (Server Component)
import { notFound } from "next/navigation";
import { AddToCart } from "./add-to-cart"; // has "use client" at the top
 
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const res = await fetch(`https://api.example.com/products/${slug}`);
  if (!res.ok) notFound();
  const product: { id: string; name: string; description: string } = await res.json();
 
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCart productId={product.id} />
    </article>
  );
}

03Стратегии рендеринга: SSG, SSR, ISR и стриминг

Next.js выбирает, как рендерить каждый маршрут. Статическая генерация (SSG) собирает HTML во время сборки; это самый быстрый вариант, отдаётся с CDN и идеально подходит для маркетинговых страниц и постов в блоге. generateStaticParams сообщает Next.js, какие динамические страницы, например slug'и товаров, собрать заранее. Серверный рендеринг (SSR) рендерит на каждый запрос — это нужно, когда страница зависит от cookie, заголовков или данных конкретного пользователя. Incremental Static Regeneration (ISR) — золотая середина: отдаётся статическая страница, а в фоне она перегенерируется через заданное время (export const revalidate = 3600) или когда вы вызываете revalidatePath или revalidateTag после изменения контента. Стриминг позволяет серверу сразу отправить каркас страницы, а более медленные части дозаполнить позже — через границы Suspense и loading.tsx. Partial Prerendering сочетает оба подхода: статический каркас плюс динамические «дыры»; в Next.js 16 он включается через модель Cache Components.

tsx
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // ISR: regenerate at most once an hour
 
export async function generateStaticParams() {
  const posts: { slug: string }[] = await fetch("https://cms.example.com/posts")
    .then((r) => r.json());
  return posts.map((p) => ({ slug: p.slug })); // pre-built at build time
}
 
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
 
export async function publishPost(slug: string) {
  // ...save to the CMS or database...
  revalidatePath(`/blog/${slug}`); // refresh that page on the next request
}

04Кэширование и ревалидация, и что изменилось в Next.js 16

Именно в кэшировании версии Next.js различаются сильнее всего. Next.js 14 по умолчанию кэшировал fetch-запросы, что удивило многие команды. Next.js 15 это развернул: fetch, GET-обработчики маршрутов и клиентская навигация между страницами по умолчанию больше не кэшируются, а включается кэш явно — через fetch(url, { next: { revalidate: 60, tags: ['products'] } }), unstable_cache или опции сегмента маршрута. Next.js 16 представил Cache Components, которые включаются через cacheComponents: true. Вместо того чтобы угадывать, что статично, вы явно помечаете кэшируемую работу директивой "use cache" на функции, компоненте или странице, задаёте время жизни через cacheLife и метку через cacheTag; всё некэшируемое должно находиться внутри границы Suspense и рендерится во время запроса. Изменилась и инвалидация: revalidateTag теперь принимает вторым аргументом профиль cacheLife (stale-while-revalidate), новый updateTag сразу инвалидирует и обновляет данные внутри Server Action (read-your-writes), а refresh() обновляет клиентский роутер.

ts
// next.config.ts (Next.js 16)
// const nextConfig = { cacheComponents: true };
 
// lib/products.ts
import { cacheLife, cacheTag } from "next/cache";
 
export async function getProducts() {
  "use cache";
  cacheLife("hours");          // built-in profile
  cacheTag("products");
  const res = await fetch("https://api.example.com/products");
  return res.json();
}
 
// app/admin/actions.ts
"use server";
import { revalidateTag, updateTag } from "next/cache";
 
export async function onCatalogImport() { revalidateTag("products", "max"); } // background refresh
export async function onPriceEdit() { updateTag("products"); } // admin sees change now

05Метаданные и SEO

Поисковики и соцсети читают HTML-элемент <head>, и App Router генерирует его из вашего кода. Экспортируйте из любого layout или page статический объект metadata или асинхронную функцию generateMetadata для динамических страниц. Задайте шаблон заголовка в корневом layout ("%s | Example Shop"), чтобы каждая страница получала единообразный суффикс, и один раз установите metadataBase, чтобы относительные URL изображений Open Graph становились абсолютными. Для каждой страницы укажите уникальные title и description, канонический URL, чтобы избежать дублированного контента, и alternates.languages (hreflang) для переведённых версий. Остальное покрывают файловые соглашения: sitemap.ts, robots.ts и opengraph-image.tsx генерируют эти файлы во время сборки или запроса. Добавьте на страницу структурированные данные (JSON-LD, например Product или Article) в теге script, чтобы поисковики могли показывать расширенные сниппеты. Поскольку HTML рендерится на сервере, краулеры видят полный контент без выполнения JavaScript.

tsx
// app/products/[slug]/page.tsx
import type { Metadata } from "next";
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const product = await fetch(`https://api.example.com/products/${slug}`).then((r) => r.json());
  return {
    title: product.name, // root layout template adds " | Example Shop"
    description: product.summary,
    alternates: {
      canonical: `/products/${slug}`,
      languages: { en: `/en/products/${slug}`, ru: `/ru/products/${slug}` },
    },
    openGraph: { images: [product.imageUrl] },
  };
}

06Изображения, шрифты и почему сайты на Next.js быстрые

Большие изображения и веб-шрифты — обычные причины медленных страниц и сдвигов макета. next/image по запросу изменяет размер изображений и конвертирует их в современные форматы вроде WebP или AVIF, генерирует srcset, чтобы телефоны скачивали маленькие файлы, лениво загружает изображения ниже первого экрана и резервирует место через width и height (или fill с родителем заданного размера), чтобы макет не прыгал. Главное изображение, которое обычно и является Largest Contentful Paint, пометьте для немедленной загрузки: priority в Next.js 15, заменённый на preload в Next.js 16. next/font скачивает Google- или локальные шрифты во время сборки и отдаёт их с вашего собственного домена, без запросов к Google во время работы, и генерирует fallback с подогнанным размером, чтобы текст не «переливался» при загрузке шрифта. Вместе с серверным рендерингом, автоматическим code splitting по маршрутам, prefetch ссылок и стримингом именно поэтому сайты на Next.js из коробки хорошо проходят Core Web Vitals.

tsx
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin", "cyrillic"], display: "swap", variable: "--font-main" });
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}
 
// app/page.tsx
import Image from "next/image";
export default function Home() {
  return (
    <Image src="/hero.jpg" alt="Fresh coffee beans" width={1600} height={900}
      sizes="100vw" preload /> // Next.js 15: use priority instead of preload
  );
}

07Варианты деплоя и Next.js 15 против 16

Vercel, компания, стоящая за Next.js, предлагает хостинг без настройки с CDN, serverless-функциями и оптимизацией изображений. Другие платформы (Netlify, Cloudflare через OpenNext, AWS Amplify) поддерживают его через адаптеры; в Next.js 16.2 для этого стабилизирован Build Adapters API. Можно и хостить самостоятельно: next build, затем next start запускает Node.js-сервер, обычно под управлением pm2 или в контейнере, за nginx для TLS и сжатия. Настройка output: "standalone" создаёт минимальную папку, идеальную для Docker-образов. При самостоятельном хостинге с несколькими инстансами настройте общий cache handler, чтобы данные ISR оставались согласованными. Ключевые изменения Next.js 16 по сравнению с 15: Turbopack — бандлер по умолчанию для dev и build, middleware.ts переименован в proxy.ts (рантайм Node.js), синхронный доступ к params и cookies() удалён, требуется Node.js 20.9+, next lint убран, а значения по умолчанию у next/image строже (только quality 75 и более долгое минимальное время кэширования).

ts
// next.config.ts - self-hosted build for Docker
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  output: "standalone",
  images: {
    remotePatterns: [{ protocol: "https", hostname: "cdn.example.com" }],
  },
};
export default nextConfig;
 
// Build and run:
//   npm ci && npm run build
//   node .next/standalone/server.js   (copy public/ and .next/static/ alongside)
// Behind nginx: proxy_pass http://127.0.0.1:3000;

Когда применять

  • Публичные сайты, где важны SEO и скорость первой загрузки: интернет-магазины, маркетинговые сайты, блоги, документация.
  • Продукты, сочетающие статический контент с персонализированными или динамическими частями, — например, магазин с корзиной и личным кабинетом.
  • Команды, уже использующие React, которым нужны готовые соглашения по маршрутизации, рендерингу и производительности, а не ручная сборка.
  • Full-stack-функции с лёгким бэкендом: формы, вебхуки и небольшие API через Server Actions и route handlers.

Когда не стоит

  • Внутренние дашборды за логином без требований к SEO, где обычное SPA на React + Vite проще сделать и захостить.
  • Тяжёлые бэкенд-домены (сложные бизнес-правила, очереди, множество интеграций); держите отдельный API (Laravel, NestJS), а Next.js используйте как фронтенд.
  • Команды, которые нигде не могут запускать Node.js и которым нужны только статические страницы; генератор статических сайтов вроде Astro может оказаться легче.

Альтернативы

  • Remix / React Router 7

    React-фреймворк, построенный вокруг веб-стандартов, loader'ов и action'ов; силён в приложениях с большим количеством данных.

  • Astro

    Сайты, ориентированные на контент, которые по умолчанию не отправляют JavaScript вообще, с «островами» на React, Vue или Svelte.

  • Nuxt или SvelteKit

    Аналогичные full-stack-фреймворки для Vue и Svelte.

  • React + Vite (SPA)

    Чисто клиентский React; проще всего для приложений с авторизацией, которым не нужно SEO.

Лучшие практики

  1. По умолчанию держите компоненты на сервере и добавляйте "use client" только интерактивным «листьям».
  2. Будьте явными в кэшировании: для каждого источника данных решите, насколько свежими они должны быть и как инвалидируются (теги, пути).
  3. Генерируйте метаданные, канонические URL, hreflang, sitemap и robots для каждой публичной страницы.
  4. Используйте next/image с правильными sizes и next/font для каждого изображения и шрифта; делайте preload только главного изображения.
  5. Оборачивайте медленные данные в границы Suspense, чтобы каркас страницы стримился сразу.
  6. Валидируйте ввод в Server Actions и route handlers точно так же, как в любом API, — это публичные эндпоинты.
  7. Фиксируйте мажорную версию и при переходе между 15 и 16 следуйте официальному гайду по обновлению и codemod'ам.

Частые ошибки

  • Ставить "use client" в начале page или layout, затягивая всё дерево в браузерный бандл.
  • Предполагать, что данные кэшируются (или не кэшируются), не проверив правила для вашей версии Next.js.
  • Читать cookies() или headers() в корневом layout и случайно делать каждую страницу динамической.
  • Использовать обычные теги <img> без размеров, вызывая сдвиги макета и скачивание слишком больших файлов.
  • Считать Server Actions приватными функциями и пропускать аутентификацию и валидацию.

Глубже

08RSC payload, клиентский роутер и его кэш

Ответ страницы Next.js содержит две вещи: HTML для первой отрисовки и RSC payload — компактное сериализованное описание дерева Server Components с плейсхолдерами на месте Client Components и ссылками на их JavaScript-файлы. При первой загрузке браузер показывает HTML, а затем React по payload гидрирует Client Components. При последующих переходах через <Link> Next.js вообще не загружает HTML: он запрашивает только RSC payload для новых сегментов маршрута и встраивает его в текущее дерево, так что общие layout'ы сохраняют своё состояние и не перерендериваются. Предзагруженные и посещённые сегменты хранятся в клиентском кэше роутера в памяти. Начиная с Next.js 15, сегменты страниц по умолчанию не переиспользуются из этого кэша (layout'ы и состояния загрузки — по-прежнему да), что защищает от устаревших страниц; это настраивается через experimental.staleTimes. После мутации вызывайте revalidatePath или updateTag на сервере либо router.refresh() на клиенте, чтобы клиентский кэш не показывал устаревшие данные. Понимание этого разделения объясняет, почему полная перезагрузка страницы иногда «чинит» баг: она сбрасывает клиентский кэш.

tsx
"use client";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
 
export function MarkShippedButton({ orderId }: { orderId: string }) {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
 
  async function markShipped() {
    await fetch(`/api/orders/${orderId}/ship`, { method: "POST" });
    startTransition(() => router.refresh()); // refetch RSC payload, keep client state
  }
 
  return <button onClick={markShipped} disabled={isPending}>Mark shipped</button>;
}
 
// next.config.ts (optional): experimental: { staleTimes: { dynamic: 30, static: 180 } }

09Server Actions и route handlers как граница безопасности

Server Action выглядит как вызов функции, но компилируется в публичный POST-эндпоинт, который кто угодно может вызвать с любыми аргументами. Next.js добавляет некоторую защиту: экшены получают неугадываемые id, неиспользуемые экшены удаляются из сборки, захваченные замыканием переменные шифруются перед отправкой на клиент, а заголовок Origin сравнивается с host, чтобы блокировать подделку межсайтовых запросов. Если вы деплоите за прокси или CDN с другим host, укажите его в experimental.serverActions.allowedOrigins, а не отключайте проверку. Тело запроса по умолчанию ограничено 1 MB (serverActions.bodySizeLimit). Ничто из этого не заменяет ваших собственных проверок: в каждом экшене аутентифицируйте пользователя, авторизуйте доступ к конкретной записи и валидируйте аргументы схемой — ровно как в REST-контроллере. Держите код доступа к данным в отдельном модуле с import 'server-only', чтобы он никогда случайно не попал в клиентский бандл. При запуске нескольких инстансов задайте на всех одинаковый NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, чтобы зашифрованные значения работали между серверами.

ts
"use server";
import "server-only";
import { z } from "zod";
import { updateTag } from "next/cache";
import { getSession } from "@/lib/auth";
import { db } from "@/lib/db";
 
const Input = z.object({ productId: z.string().uuid(), price: z.number().int().positive() });
 
export async function updatePrice(raw: unknown) {
  const session = await getSession();
  if (!session?.user.isAdmin) throw new Error("Forbidden"); // authorise every call
  const { productId, price } = Input.parse(raw);             // never trust arguments
  await db.product.update({ where: { id: productId }, data: { price } });
  updateTag(`product-${productId}`);
}

10Proxy, рантаймы и работа во время запроса

В Next.js 16 middleware.ts переименован в proxy.ts, а экспортируемая функция называется proxy. Он выполняется до маршрутизации для каждого подходящего запроса в рантайме Node.js и предназначен для лёгких решений: редиректов, rewrite'ов, определения локали, установки заголовков или быстрой проверки наличия session cookie. Он не должен быть вашим единственным слоем авторизации, потому что неправильно настроенный matcher или баг могут его пропустить; проверяйте права ещё раз рядом с данными. Держите его быстрым, ведь он добавляет задержку каждому подходящему запросу, и исключайте статические файлы и изображения через конфиг matcher. Помимо proxy, маршрут делают динамическим несколько API: cookies(), headers(), connection() и searchParams. Их вызов в корневом layout делает весь сайт рендерящимся во время запроса, поэтому читайте их как можно глубже в дереве и оборачивайте эту часть в Suspense. after() позволяет запланировать работу вроде логирования или аналитики на время после отправки ответа, чтобы она не замедляла пользователя.

ts
// proxy.ts (Next.js 16; was middleware.ts)
import { NextResponse, type NextRequest } from "next/server";
 
export function proxy(request: NextRequest) {
  const hasSession = request.cookies.has("session");
  if (!hasSession && request.nextUrl.pathname.startsWith("/account")) {
    const url = new URL("/login", request.url);
    url.searchParams.set("next", request.nextUrl.pathname);
    return NextResponse.redirect(url);
  }
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"],
};

11Self-hosting в масштабе: общие кэши, version skew и размер бандла

Один сервер Next.js на одной машине — это просто. Несколько инстансов за балансировщиком порождают три проблемы. Согласованность кэша: по умолчанию каждый инстанс хранит свой ISR и кэш данных на локальном диске и в памяти, так что пользователи могут видеть разные версии страницы в зависимости от того, какой сервер ответил. Настройте собственный cacheHandler на основе Redis или другого общего хранилища и отключите кэш в памяти через cacheMaxMemorySize: 0, если каждое чтение должно проходить через него. Version skew: во время rolling-деплоя браузер со старым JavaScript может обратиться к новому серверу. Собирайте один раз и отправляйте один и тот же артефакт на каждый инстанс, задайте deploymentId (или NEXT_DEPLOYMENT_ID), который Next.js добавляет к URL ассетов и ключам кэша, и какое-то время держите доступными статические файлы предыдущей сборки, в идеале на CDN. Размер бандла: в Next.js 16.1+ команда next experimental-analyze анализирует продакшен-бандлы Turbopack, так что можно увидеть, какой импорт затянул большую библиотеку в клиентский компонент. Кэшируйте /_next/static агрессивно, поскольку имена этих файлов содержат хэш содержимого.

ts
// next.config.ts - multi-instance self-hosting
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  output: "standalone",
  deploymentId: process.env.GIT_SHA,              // same value on every instance
  cacheHandler: require.resolve("./cache-handler.mjs"), // shared store, e.g. Redis
  cacheMaxMemorySize: 0,                          // no per-instance memory cache
};
export default nextConfig;
 
// nginx: content-hashed assets can be cached for a year
// location /_next/static/ { proxy_pass http://app; add_header Cache-Control "public, max-age=31536000, immutable"; }
 
// Inspect client bundles (Next.js 16.1+):
//   npx next experimental-analyze

Реальные ситуации

01

Цены товаров обновляются в админке, но не на витрине

Проблема

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

Как решить

Здесь сочетаются две проблемы. Страница кэшируется с revalidate по времени, и при изменении её ничто не инвалидирует, поэтому добавьте cacheTag для каждого товара и вызывайте revalidateTag (или updateTag внутри Server Action), когда админка сохраняет данные; если админка — отдельный бэкенд, откройте небольшой route handler с аутентификацией, который она сможет вызывать как вебхук. Скачки между старой и новой ценой означают несколько инстансов с локальными кэшами. Настройте общий cacheHandler в Redis, чтобы все инстансы читали одни и те же записи, и задайте везде одинаковый deploymentId. Затем проверьте: сохраните цену и несколько раз перезагрузите страницу через балансировщик.

02

После добавления бейджа корзины весь сайт стал динамическим

Проблема

Разработчик добавил в шапку счётчик корзины, читая cookies() в корневом layout. Теперь вывод сборки показывает все маршруты как динамические, time to first byte вырос вдвое, а кэширование на CDN перестало работать.

Как решить

Чтение cookies в корневом layout переводит каждую страницу на рендеринг во время запроса. Перенесите чтение cookie в небольшой компонент CartBadge и рендерьте его внутри границы Suspense с нейтральным fallback, например пустым бейджем. При включённых Cache Components статическая оболочка, включая шапку, тогда пререндерится, а бейдж стримится на каждый запрос (Partial Prerendering). Без Cache Components альтернатива — клиентский компонент, который после загрузки получает количество из route handler. Снова проверьте вывод сборки: страницы товаров и контента должны быть статическими или ISR, а динамическими — только маршруты бейджа, корзины и аккаунта.

03

Деплой на собственном хостинге ломает навигацию активным пользователям

Проблема

После каждого релиза некоторые пользователи, у которых был открыт сайт, видят ошибки при клике по ссылкам или отправке форм, пока не перезагрузят страницу. В логах — 404 на JavaScript-чанки и упавшие вызовы Server Actions.

Как решить

Это version skew: старые страницы запрашивают чанки или id экшенов, которых в новой сборке уже нет. Собирайте один раз в CI и деплойте везде один и тот же артефакт. Задайте deploymentId из git-коммита, чтобы Next.js мог обнаружить несовпадение и при необходимости перезагрузить страницу. Держите файлы /_next/static предыдущих сборок доступными несколько дней, например загружая их на CDN или в объектное хранилище с assetPrefix, чтобы старые страницы всё ещё могли загрузить свои чанки. Задайте фиксированный NEXT_SERVER_ACTIONS_ENCRYPTION_KEY для всех релизов и инстансов. Предпочитайте добавляющие изменения в сигнатурах Server Actions и после каждого релиза следите за 404 на /_next/static.

Шпаргалка

Создать новое приложение
npx create-next-app@latest my-app --ts --app
Обновиться с помощью официальных codemod'ов
npx @next/codemod@canary upgrade latest
Посмотреть, какие маршруты статические, а какие динамические
npm run build // read the route table in the output
Проанализировать клиентские бандлы (16.1+)
npx next experimental-analyze
Явно перевести компонент на время запроса
import { connection } from "next/server"; await connection();
Выполнить работу после отправки ответа
import { after } from "next/server"; after(() => logView(id));
Не допустить модуль в клиентские бандлы
import "server-only";
Обновить серверные данные с клиента
router.refresh() // from next/navigation
Разрешить Server Actions за доменом прокси
experimental: { serverActions: { allowedOrigins: ["shop.example.com"] } }
Согласованные сборки на всех инстансах
deploymentId: process.env.GIT_SHA

В моих реальных проектах

  • Я был основным контрибьютором полной переделки сайта агентства с Pages Router на App Router на Next.js 15: новая версия около полутора лет создавалась параллельно, а затем заменила старую.
  • Интернет-магазин зоотоваров работает на Next.js 16 с next-intl на трёх языках, SEO для каждой страницы и sitemap, а бэкендом служит API на Laravel.
  • Карьерный сайт сети кофеен работает на Next.js 16 с собственным сервером для заголовков безопасности и кэширования, плюс сгенерированные sitemap, robots и изображения Open Graph.
  • На моих собственных серверах приложения на Next.js работают под pm2 за nginx рядом с API на Laravel, которые их питают.

Вопросы и ответы

Q1Чем отличаются SSR, SSG и ISR?

Они различаются тем, когда генерируется HTML. SSG рендерит страницы во время сборки, поэтому каждый посетитель получает один и тот же заранее собранный файл с CDN — это самый быстрый вариант, подходящий для редко меняющегося контента. SSR рендерит на каждый запрос, что нужно для персонализированных страниц или данных, которые каждый раз должны быть свежими, ценой серверной работы на каждый визит. ISR объединяет их: страницы статические, но перегенерируются в фоне через заданный интервал или по требованию при изменении контента — через revalidatePath или revalidateTag. В App Router режим редко выбирают по названию: он следует из используемых данных, настроек кэширования и динамических функций вроде cookies().

Q2Как Server и Client Components работают вместе в Next.js?

Всё в директории app — Server Component, если не помечено иначе. Server Components рендерятся на сервере, могут загружать данные напрямую и держат секреты и тяжёлые библиотеки вне браузера. Файл, начинающийся с "use client", становится клиентской границей: он и его импорты попадают в бандл для браузера, гидрируются и могут использовать состояние, эффекты и события. Server Components могут рендерить Client Components и передавать им сериализуемые props или отрендеренных на сервере children, но Client Components не могут напрямую импортировать Server Components. Цель — держать интерактивные части маленькими, чтобы страницы отправляли меньше JavaScript и при этом оставались полностью интерактивными там, где нужно.

Q3Почему сайты на Next.js хороши для SEO?

Главным образом потому, что краулеры получают полный HTML. При статическом или серверном рендеринге заголовки, текст, ссылки и структурированные данные есть уже в первоначальном ответе, поэтому поисковикам не нужно выполнять JavaScript, чтобы увидеть контент. Кроме того, Next.js даёт встроенные инструменты для остального технического SEO: Metadata API для title, description, канонических URL, hreflang и тегов Open Graph; файловые соглашения для sitemap.xml и robots.txt; генерируемые изображения для соцсетей; и сильные Core Web Vitals благодаря оптимизации изображений, работе со шрифтами, code splitting и стримингу. Хорошему SEO всё равно нужны хороший контент и структура, но фреймворк убирает большую часть технических препятствий.

Q4Что изменилось в кэшировании между Next.js 14, 15 и 16?

Next.js 14 по умолчанию кэшировал результаты fetch и обработчики маршрутов, из-за чего данные неожиданно устаревали. Next.js 15 развернул эти значения по умолчанию: fetch, GET-обработчики маршрутов и клиентская навигация к страницам не кэшируются, пока вы не включите это явно через revalidate, теги, unstable_cache или конфигурацию сегмента. Next.js 16 добавляет Cache Components за флагом cacheComponents: true, где кэширование явное: вы помечаете функции или компоненты через "use cache", задаёте время жизни через cacheLife и метки через cacheTag, а некэшируемые данные должны находиться внутри Suspense. revalidateTag теперь требует профиль cacheLife, updateTag даёт обновление в духе read-your-writes в Server Actions, а Partial Prerendering включается через эту модель.

Q5Стоит ли обновляться с Next.js 15 на 16?

Обычно да, но как запланированная задача. Next.js 16 приносит Turbopack в качестве бандлера по умолчанию с гораздо более быстрыми сборками, стабильную опцию React Compiler, переработанный роутер с более экономным prefetch, возможности React 19.2 и модель Cache Components. Ломающие изменения вполне управляемы: Node.js 20.9 или новее, только асинхронные params, cookies() и headers(), middleware переименован в proxy, next lint удалён, значения по умолчанию у next/image строже, а у revalidateTag новая сигнатура. Официальный codemod берёт на себя большую часть этого. Я обновляюсь в отдельной ветке, запускаю codemod'ы, проверяю кастомную конфигурацию webpack и использование изображений, затем тестирую критичные сценарии. Версия 15 по-прежнему получает бэкпорты исправлений, так что спешить некуда.

Q6Как задеплоить приложение на Next.js не на Vercel?

Собрать с output: "standalone", который создаёт минимальный Node.js-сервер только с нужными зависимостями, а затем запустить его в Docker-контейнере или под менеджером процессов вроде pm2. Поставить перед ним nginx для TLS, сжатия и кэширования неизменяемых файлов /_next/static, а при необходимости перед ним ещё и CDN вроде Cloudflare. Задавать переменные окружения во время запуска, не держать секреты в переменных NEXT_PUBLIC_ и добавить маршрут для health check. Оптимизация изображений работает на сервере, но под нагрузкой помогает CDN. Если запускаете несколько инстансов, настройте общий cache handler, например в Redis, чтобы ISR и кэшированные данные оставались согласованными между ними.

Вопросы уровня senior

S1Как бы вы спроектировали кэширование для большого e-commerce сайта на Next.js?

Я определяю свежесть по источнику данных, а не по странице. Контент категорий и товаров можно кэшировать часами и инвалидировать по тегам при изменении каталога — через вебхук от бэкенда. Цены и остатки меняются чаще и важнее, поэтому получают короткое время жизни или загружаются во время запроса в небольшом компоненте, обёрнутом в Suspense, а остальная страница остаётся статической. Корзина, аккаунт и checkout всегда динамические и никогда не кэшируются на общих слоях. С Cache Components это напрямую ложится на "use cache" с cacheLife и cacheTag плюс Partial Prerendering для оболочки. На собственной инфраструктуре общий cache handler поддерживает согласованность инстансов, а CDN кэширует статические ассеты и публичные страницы. Компромисс — сложность: у каждого закэшированного элемента должны быть понятный владелец и путь инвалидации, иначе устаревшие цены превращаются в проблему для поддержки.

S2Когда вы не стали бы использовать Server Actions, а выбрали бы route handlers или отдельный API?

Server Actions отлично подходят для мутаций, которые инициирует ваш собственный UI на Next.js, особенно для форм, потому что интегрируются с transitions, progressive enhancement и инвалидацией кэша. Они плохо подходят, когда ту же операцию должны выполнять другие клиенты: мобильному приложению, партнёрской интеграции или вебхуку нужен стабильный задокументированный HTTP-контракт, который дают route handlers или отдельный бэкенд. Id экшенов зависят от конкретной сборки, поэтому это не публичный API. Кроме того, по умолчанию они выполняются последовательно с одного клиента, так что плохо подходят для параллельной загрузки данных; чтение данных — задача Server Components. Когда бизнес-домен большой — с очередями, задачами по расписанию и множеством интеграций, — я держу эту логику в отдельном API на Laravel или NestJS, а Server Actions делаю тонкими обёртками, которые его вызывают.

S3Vercel или собственный хостинг: что вы посоветуете клиенту?

Это компромисс между операционными усилиями и стоимостью с контролем. Vercel даёт полный набор возможностей без настройки: глобальный CDN, оптимизацию изображений, preview-деплои, защиту от version skew и масштабирование. Он идеален для небольших команд и трафика с пиками, но расходы растут вместе с трафиком, временем работы функций и преобразованиями изображений, а требования к размещению данных или существующая инфраструктура могут его исключить. Собственный хостинг на VPS или в контейнерах с nginx и CDN дешевле при стабильном трафике и держит всё в одном месте, но команда отвечает за то, что Vercel автоматизирует: TLS, деплои без простоя, общие кэши между инстансами, version skew, сбор логов и мониторинг. Другие платформы с адаптерами находятся посередине. Для клиента с собственной серверной командой и предсказуемым трафиком я склоняюсь к собственному хостингу; для небольшой маркетинговой команды обычно выигрывает управляемая платформа.

Глоссарий

RSC payload
Сериализованное дерево Server Components, которое клиентский роутер использует, чтобы обновить страницу без загрузки HTML.
Кэш клиентского роутера
Память в браузере о предзагруженных и посещённых сегментах маршрутов, благодаря которой навигация мгновенна.
Dynamic API
Функция вроде cookies(), headers() или connection(), которая принудительно включает рендеринг во время запроса там, где используется.
Partial Prerendering
Мгновенная отдача пререндеренной статической оболочки и стриминг динамических частей в «дыры» Suspense на каждый запрос.
Тег кэша
Метка, прикреплённая к закэшированным данным, чтобы все записи с этой меткой можно было инвалидировать разом.
Proxy (proxy.ts)
Название middleware в Next.js 16: код, который выполняется до маршрутизации, чтобы сделать редирект, rewrite или задать заголовки.
Server Action
Серверная функция, вызываемая с клиента и компилируемая в POST-эндпоинт; обязана сама валидировать данные и проверять права.
Version skew
Браузер с кодом старой сборки общается с серверами, на которых работает новая сборка, из-за чего не находятся чанки или экшены.
Standalone output
Режим сборки, который копирует только файлы и зависимости, нужные для запуска сервера; идеален для Docker.
Cache handler
Подключаемый модуль, который хранит записи кэша Next.js, например в Redis, общий для всех инстансов.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.