29. Next.js 15: новые возможности
🆕 Next.js 15: Всё, что тебе нужно знать
Заголовок раздела «🆕 Next.js 15: Всё, что тебе нужно знать»Привет! 👋 Next.js 15 вышел и принёс с собой несколько изменений, которые ломают обратную совместимость. Паниковать не нужно — большинство изменений можно мигрировать автоматически. Но понять их важно, иначе приложение начнёт вести себя странно.
Думай об этом как об обновлении операционной системы 💻. Большинство программ работает как раньше, но некоторые API изменились, и нужно обновить свои привычки. Погнали разбираться!
🗺️ Обзор Next.js 15: что нового и почему это важно
Заголовок раздела «🗺️ Обзор Next.js 15: что нового и почему это важно»Next.js 15 — это не революция, а эволюция. Команда Vercel исправила несколько архитектурных решений, которые создавали неожиданное поведение:
- ⚡ Async Request APIs —
cookies(),headers(),paramsтеперь возвращают Promise - 🗄️ Изменения кэширования —
fetchбольше НЕ кэшируется автоматически - 🚀 Turbopack стабилен —
next dev --turboвыходит из beta - 🔀 Partial Prerendering — PPR экспериментально стабилизирован
- ⏰
unstable_after()— выполнение кода после отправки response - 📝 Новый
<Form>— умный компонент формы с prefetch - ⚛️ React 19 — полная поддержка React 19 с новыми хуками
- 📁
next.config.ts— TypeScript конфиг вместо.mjs
Версия React минимум — 18.2.0, но рекомендуется 19.x.
⚡ Async Request APIs: самое важное изменение
Заголовок раздела «⚡ Async Request APIs: самое важное изменение»Это самое важное breaking change в Next.js 15. Функции cookies(), headers(), params и searchParams теперь асинхронные!
Почему это сломает твой код:
// ❌ Next.js 14 — синхронный доступ (скоро устареет!)import { cookies, headers } from 'next/headers';
export default function Page({ params }: { params: { id: string } }) { const cookieStore = cookies(); // синхронно const headersList = headers(); // синхронно const token = cookieStore.get('auth-token'); const id = params.id; // синхронно
return <div>User ID: {id}</div>;}// ✅ Next.js 15 — асинхронный доступimport { cookies, headers } from 'next/headers';
export default async function Page({ params,}: { params: Promise<{ id: string }>;}) { const cookieStore = await cookies(); // обязательный await const headersList = await headers(); // обязательный await const token = cookieStore.get('auth-token'); const { id } = await params; // await params тоже!
return <div>User ID: {id}</div>;}Это касается всех API динамических данных:
import { cookies, headers } from 'next/headers';import type { NextRequest } from 'next/server';
// Все эти API теперь асинхронные:export default async function ProductPage({ params, searchParams,}: { params: Promise<{ id: string; category: string }>; searchParams: Promise<{ sort: string; page: string }>;}) { // params — динамические сегменты пути const { id, category } = await params;
// searchParams — параметры строки запроса const { sort, page } = await searchParams;
// cookies и headers — тоже async const cookieStore = await cookies(); const headersList = await headers();
const userAgent = headersList.get('user-agent'); const session = cookieStore.get('session');
return ( <div> <h1>Продукт {id} в категории {category}</h1> <p>Сортировка: {sort}, Страница: {page}</p> </div> );}🤔 Почему сделали Async? Философия изменения
Заголовок раздела «🤔 Почему сделали Async? Философия изменения»Это может казаться усложнением, но за этим стоит важная причина!
В Next.js 14 cookies() и headers() были синхронными, но при этом делали весь роут динамическим — даже если ты только импортировал их, но не вызывал!
// Next.js 14: ВЕСЬ роут становится динамическим из-за импорта!import { cookies } from 'next/headers'; // ← импорт = динамический роут
export default async function Page() { // Даже если ты не вызываешь cookies() — // Next.js всё равно не может статически рендерить эту страницу const data = await fetch('https://api.example.com/static-data'); return <div>{data}</div>;}С асинхронными API: динамичность происходит только в момент вызова await cookies(). Это открывает путь к Partial Prerendering (PPR), где статическая и динамическая части могут сосуществовать!
🔧 Практика миграции: await cookies(), await headers(), await params
Заголовок раздела «🔧 Практика миграции: await cookies(), await headers(), await params»Вот конкретные сценарии миграции:
Middleware — без изменений! (уже использовал async)
// middleware.ts — не меняетсяimport { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) { // request.cookies — всё ещё синхронный в middleware const token = request.cookies.get('token'); if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next();}Route Handlers:
import { cookies, headers } from 'next/headers';import { NextResponse } from 'next/server';
export async function GET() { const cookieStore = await cookies(); // await! const headersList = await headers(); // await!
const session = cookieStore.get('session')?.value; const contentType = headersList.get('content-type');
if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
return NextResponse.json({ session, contentType });}Server Actions:
'use server';import { cookies } from 'next/headers';import { redirect } from 'next/navigation';
export async function login(formData: FormData) { const email = formData.get('email') as string; const password = formData.get('password') as string;
const user = await authenticate(email, password);
if (user) { const cookieStore = await cookies(); // await! cookieStore.set('session', user.token, { httpOnly: true, secure: true, maxAge: 60 * 60 * 24 * 7, // 7 дней }); redirect('/dashboard'); }}🤖 @next/codemod: автоматическая миграция
Заголовок раздела «🤖 @next/codemod: автоматическая миграция»Не хочешь менять вручную? Next.js предлагает автоматический codemod!
npx @next/codemod@canary upgrade latest
# Только конкретный codemod для async APInpx @next/codemod@canary next-async-request-api .
# Проверить что изменится (dry-run)npx @next/codemod@canary next-async-request-api . --dry
# С подробным выводомnpx @next/codemod@canary next-async-request-api . --verboseЧто делает codemod:
// До запуска codemodexport default function Page({ params }: { params: { id: string } }) { const id = params.id; return <div>{id}</div>;}
// После запуска codemod (автоматически!)export default async function Page({ params,}: { params: Promise<{ id: string }>;}) { const { id } = await params; return <div>{id}</div>;}💡 Совет: Codemod хорошо справляется с очевидными случаями, но может не распознать сложные паттерны. После запуска проверь изменения вручную!
⚛️ React 19: поддержка и новые хуки
Заголовок раздела «⚛️ React 19: поддержка и новые хуки»Next.js 15 полностью поддерживает React 19. Давай посмотрим на самые полезные новинки.
useActionState (бывший useFormState):
// Улучшенная обработка состояния формы'use client';import { useActionState } from 'react'; // из react, не из react-dom!import { createPost } from '@/app/actions';
type FormState = { success: boolean; error?: string; post?: { id: string; title: string };};
const initialState: FormState = { success: false };
export function CreatePostForm() { const [state, formAction, isPending] = useActionState( createPost, initialState );
return ( <form action={formAction}> {state.error && ( <div className="error">{state.error}</div> )} {state.success && ( <div className="success">Пост создан: {state.post?.title}</div> )} <input name="title" placeholder="Заголовок" disabled={isPending} /> <textarea name="content" placeholder="Текст" disabled={isPending} /> <button type="submit" disabled={isPending}> {isPending ? '⏳ Создаём...' : '✨ Создать пост'} </button> </form> );}useFormStatus:
// Компонент знает о состоянии родительской формы'use client';import { useFormStatus } from 'react-dom';
function SubmitButton() { const { pending, data, method, action } = useFormStatus();
return ( <button type="submit" disabled={pending} style={{ opacity: pending ? 0.6 : 1 }} > {pending ? '⏳ Отправляем...' : '🚀 Отправить'} </button> );}
// Используется внутри <form>export function MyForm() { return ( <form action={someServerAction}> <input name="email" type="email" /> <SubmitButton /> {/* автоматически получает pending состояние */} </form> );}use() хук для Promise:
// use() — разворачивает Promise или Context в рендере'use client';import { use, Suspense } from 'react';
// Можно передать Promise как проп из Server Component!function UserProfile({ userPromise }: { userPromise: Promise<User> }) { const user = use(userPromise); // разворачивает Promise синхронно! return <div>{user.name}</div>;}
// Server Component передаёт Promise клиентуexport default function Page() { const userPromise = getUser(); // не await! return ( <Suspense fallback={<div>Загрузка...</div>}> <UserProfile userPromise={userPromise} /> </Suspense> );}🚀 Turbopack Dev: стабильный и быстрый
Заголовок раздела «🚀 Turbopack Dev: стабильный и быстрый»Turbopack — это новый бандлер, написанный на Rust. В Next.js 15 он официально стабилен для next dev!
# Next.js 15: стабильный Turbopacknpx next dev --turbo
# Или в package.json:# "dev": "next dev --turbo"Сравнение производительности (реальные проекты):
| Метрика | Webpack | Turbopack | Улучшение |
|---|---|---|---|
| Cold start (большой проект) | ~8-15s | ~1-3s | 5x быстрее |
| HMR update | ~500ms-2s | ~50-200ms | 7x быстрее |
| Initial compilation | ~5-12s | ~1-2.5s | 5x быстрее |
| Использование памяти | ~1-2 GB | ~400-600 MB | 3x меньше |
Как это работает:
Webpack (JavaScript): Изменение файла → пересборка зависимостей → обновление Время: медленно, линейно растёт с проектом
Turbopack (Rust, инкрементальный): Изменение файла → только затронутые модули Время: стабильно быстро независимо от размера проекта⚠️ Важно: Turbopack стабилен только для
next dev. Дляnext buildвсё ещё используется webpack. Поддержкаnext build --turbo— в планах!
Несовместимые плагины webpack:
// next.config.ts — некоторые webpack плагины не работают с Turbopackimport type { NextConfig } from 'next';
const nextConfig: NextConfig = { // Turbopack не поддерживает webpack плагины напрямую // Используй turbopack-специфичные алиасы если нужно: turbopack: { resolveAlias: { '@/utils': './src/utils', }, rules: { '*.svg': { loaders: ['@svgr/webpack'], as: '*.js', }, }, },};
export default nextConfig;🔀 Partial Prerendering (PPR): статическая оболочка + динамические дыры
Заголовок раздела «🔀 Partial Prerendering (PPR): статическая оболочка + динамические дыры»PPR — это одна из самых инновационных концепций в Next.js. Представь страницу как Swiss cheese 🧀: большая часть статическая (предсгенерированная), а “дырки” — это динамические части, которые стримятся при запросе.
Проблема без PPR:
Статическая страница (fast): /about, /blog — весь HTML предсгенерированДинамическая страница (slow): /dashboard — нужно ждать ответа сервера
Что если нам нужно и то, и другое на одной странице?Product Page: заголовок статический + корзина динамическая → всё медленно!PPR решает это:
// next.config.ts — включаем PPR экспериментальноimport type { NextConfig } from 'next';
const nextConfig: NextConfig = { experimental: { ppr: 'incremental', // включаем постепенно (per-route) },};
export default nextConfig;// app/products/[id]/page.tsx — PPR в действииimport { Suspense } from 'react';import { unstable_noStore as noStore } from 'next/cache';
// Оптимистично включаем PPR для этого роутаexport const experimental_ppr = true;
// Эта функция статическая — рендерится при сборкеasync function ProductInfo({ id }: { id: string }) { const product = await fetch('https://api.example.com/products/' + id, { next: { revalidate: 3600 }, // статические данные }).then(r => r.json());
return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <p className="price">{product.price} ₽</p> </div> );}
// Эта функция динамическая — выполняется при каждом запросеasync function PersonalizedRecommendations() { noStore(); // явно помечаем как динамическую const recommendations = await getPersonalizedRecs(); // использует cookies return <div>{recommendations.map(r => <ProductCard key={r.id} {...r} />)}</div>;}
// Основная страница — статическая оболочка + динамические "дыры"export default async function ProductPage({ params,}: { params: Promise<{ id: string }>;}) { const { id } = await params;
return ( <div> {/* Статическая часть — рендерится при сборке */} <ProductInfo id={id} />
{/* Динамическая "дыра" — стримится при запросе */} <Suspense fallback={<RecommendationsSkeleton />}> <PersonalizedRecommendations /> </Suspense> </div> );}Визуально PPR работает так:
При сборке (build time): ┌─────────────────────────────────┐ │ <ProductInfo> │ ← Статический HTML │ Название: iPhone 15 Pro │ │ Цена: 99 990 ₽ │ │ </ProductInfo> │ │ <Suspense fallback={skeleton}> │ ← "Дыра" в HTML │ ░░░░░░░░░░░░░░░░░░░░░░░░ │ │ </Suspense> │ └─────────────────────────────────┘
При запросе пользователя (request time): 1. Статический HTML приходит МГНОВЕННО 2. "Дыра" заполняется динамическим контентом через стриминг🗄️ Изменения кэширования в Next.js 15
Заголовок раздела «🗄️ Изменения кэширования в Next.js 15»Это второе важное breaking change. В Next.js 14 fetch кэшировался по умолчанию. В Next.js 15 — нет!
Next.js 14 (кэш по умолчанию):
// Next.js 14: АВТОМАТИЧЕСКОЕ кэширование!export default async function Page() { // Этот запрос кэшируется навсегда (cache: 'force-cache' по умолчанию) const data = await fetch('https://api.example.com/data');
// Чтобы НЕ кэшировать — нужно явно указать const fresh = await fetch('https://api.example.com/fresh', { cache: 'no-store', // отключаем кэш });
return <div>{/* ... */}</div>;}Next.js 15 (нет кэша по умолчанию):
// Next.js 15: fetch НЕ кэшируется по умолчанию!export default async function Page() { // Этот запрос ДИНАМИЧЕСКИЙ — выполняется при каждом запросе const data = await fetch('https://api.example.com/data');
// Для кэширования теперь нужно явно указать: const cached = await fetch('https://api.example.com/cached', { cache: 'force-cache', // или next: { revalidate: 3600 } });
// ISR — обновлять каждый час const isr = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 }, });
return <div>{/* ... */}</div>;}Таблица изменений кэша:
| Поведение | Next.js 14 | Next.js 15 |
|---|---|---|
fetch() без опций | force-cache (кэш) | no-store (без кэша) |
| GET Route Handlers | Кэшируется | НЕ кэшируется |
| Client Router Cache | Кэш на 30 сек | Нет кэша по умолчанию |
cache: 'force-cache' | Работает | Работает |
next: { revalidate } | Работает | Работает |
Как адаптировать существующий код:
// Если раньше полагался на автокэш — нужно добавить явно:const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 }, // ISR: обновлять каждый час});
// Или настроить кэш на уровне роута:export const revalidate = 3600; // все fetch в этом файле кэшируются 1 часexport const dynamic = 'force-static'; // весь роут статический
// Или через fetchCache:export const fetchCache = 'force-cache'; // старое поведение Next.js 14⏰ unstable_after(): код после отправки Response
Заголовок раздела «⏰ unstable_after(): код после отправки Response»Часто нужно выполнить что-то после того, как пользователь уже получил ответ: логирование, аналитика, очистка кэша. unstable_after() позволяет это!
Без unstable_after — пользователь ждёт всего:
// ❌ Аналитика блокирует ответ пользователю!export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const product = await getProduct(id);
// Пользователь ждёт, пока мы пишем в базу аналитики... await logPageView({ page: '/products/' + id, productId: id }); await updatePopularProducts(id);
return <ProductPage product={product} />;}С unstable_after — пользователь получает ответ сразу:
// ✅ Аналитика выполняется ПОСЛЕ ответа!import { unstable_after as after } from 'next/server';
export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const product = await getProduct(id);
// Регистрируем задачу — она выполнится после response after(async () => { // Пользователь уже видит страницу, пока это выполняется await logPageView({ page: '/products/' + id, productId: id }); await updatePopularProducts(id); await sendAnalyticsEvent('product_view', { id }); });
// Ответ отправляется немедленно! return <ProductPage product={product} />;}after() также работает в Route Handlers и Server Actions:
import { unstable_after as after } from 'next/server';import { NextResponse } from 'next/server';
export async function POST(request: Request) { const order = await createOrder(await request.json());
after(async () => { // Email, уведомления, обновление статистики — после response await sendOrderConfirmationEmail(order); await updateInventory(order.items); await notifyWarehouse(order); });
// Клиент получает ответ сразу return NextResponse.json(order, { status: 201 });}⚠️ Ограничение:
after()не работает в static rendering. Только для динамических страниц и роутов.
🔬 instrumentation.ts: улучшения мониторинга
Заголовок раздела «🔬 instrumentation.ts: улучшения мониторинга»Файл instrumentation.ts позволяет подключиться к жизненному циклу Next.js сервера. В Next.js 15 он стал более стабильным.
// instrumentation.ts (в корне проекта)export async function register() { // Выполняется один раз при старте сервера if (process.env.NEXT_RUNTIME === 'nodejs') { // Node.js runtime — для тяжёлой инициализации const { NodeSDK } = await import('@opentelemetry/sdk-node'); const sdk = new NodeSDK({ // конфигурация OpenTelemetry }); sdk.start(); }
if (process.env.NEXT_RUNTIME === 'edge') { // Edge runtime — лёгкая инициализация await import('./lib/edge-monitoring'); }}
// Новое в Next.js 15: onRequestErrorexport async function onRequestError( error: Error, request: { path: string; method: string }, context: { routerKind: string; routePath: string }) { // Вызывается при каждой ошибке запроса await reportErrorToMonitoring({ error: error.message, stack: error.stack, path: request.path, method: request.method, });}📝 Новый <Form> компонент (next/form)
Заголовок раздела «📝 Новый <Form> компонент (next/form)»next/form — это умный компонент, который расширяет HTML <form> для Next.js. Он автоматически обрабатывает prefetch и soft navigation!
Обычная HTML форма vs next/form:
// ❌ Обычная HTML форма — full page reload при отправкеexport default function SearchPage() { return ( <form action="/search"> <input name="q" placeholder="Поиск..." /> <button type="submit">Найти</button> {/* При отправке → полная перезагрузка страницы */} </form> );}// ✅ next/form — soft navigation + prefetch!import Form from 'next/form';
export default function SearchPage() { return ( <Form action="/search"> {/* - При hover/focus на input → prefetch layout страницы /search - При submit → soft navigation (без перезагрузки) - Параметры формы → URL search params (?q=запрос) - Работает с Server Actions тоже! */} <input name="q" placeholder="Поиск..." /> <button type="submit">Найти</button> </Form> );}next/form с Server Actions:
import Form from 'next/form';import { searchProducts } from '@/app/actions';
export default function SearchPage({ searchParams,}: { searchParams: Promise<{ q?: string }>;}) { return ( <div> <Form action={searchProducts}> <input name="q" placeholder="Найти продукты..." autoComplete="off" /> <select name="category"> <option value="">Все категории</option> <option value="electronics">Электроника</option> <option value="clothing">Одежда</option> </select> <button type="submit">🔍 Поиск</button> </Form> </div> );}📁 TypeScript конфигурация: next.config.ts
Заголовок раздела «📁 TypeScript конфигурация: next.config.ts»Next.js 15 добавил нативную поддержку TypeScript для конфигурационного файла!
До: next.config.mjs (JavaScript):
/** @type {import('next').NextConfig} */const nextConfig = { experimental: { ppr: true, }, images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com' }, ], },};
export default nextConfig;После: next.config.ts (TypeScript):
// next.config.ts — нативный TypeScript!import type { NextConfig } from 'next';
const nextConfig: NextConfig = { experimental: { ppr: 'incremental', // TypeScript подскажет доступные поля с автодополнением! }, images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com', port: '', pathname: '/images/**', }, ], }, turbopack: { resolveAlias: { '@/utils': './src/utils/index.ts', }, }, // TypeScript проверяет типы — нельзя написать неверные опции! // Ошибка компиляции сразу укажет на проблему};
export default nextConfig;💡 Бонус: Теперь в
next.config.tsможно импортировать типы и утилиты из TypeScript-проекта. Никаких проблем сrequirevsimport!
🔄 Обновление с Next.js 14 → 15
Заголовок раздела «🔄 Обновление с Next.js 14 → 15»Пошаговое руководство:
Шаг 1: Обновить зависимости:
# Автоматическое обновление через codemodnpx @next/codemod@canary upgrade latest
# Или вручнуюnpm install next@15 react@19 react-dom@19npm install --save-dev @types/react@19 @types/react-dom@19Шаг 2: Запустить автомиграцию:
# Мигрирует async APIs автоматическиnpx @next/codemod@canary next-async-request-api .
# Мигрирует устаревший useFormState → useActionStatenpx @next/codemod@canary next-form-request-api .Шаг 3: Проверить кэширование:
// Проверь все fetch запросы — теперь они динамические!// Добавь явное кэширование там, где нужно:
// ISR — каждый часconst data = await fetch(url, { next: { revalidate: 3600 } });
// Статический — навсегда (до следующего деплоя)const data = await fetch(url, { cache: 'force-cache' });
// Динамический — при каждом запросе (поведение по умолчанию в Next.js 15)const data = await fetch(url); // или { cache: 'no-store' }Шаг 4: Переименовать конфиг (опционально):
# next.config.mjs → next.config.tsmv next.config.mjs next.config.ts# Добавить типы вручную или через codemodШаг 5: Протестировать:
# Запустить dev с Turbopacknpm run dev -- --turbo
# Проверить buildnpm run build
# Запустить тестыnpm test