Перейти к содержанию

Что нового в Next.js 16

Асинхронные параметры, Turbopack по умолчанию и стабилизация экспериментальных функций

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

Если вы планируете обновление, вот что изменилось и почему это важно.

Параметры маршрутов теперь асинхронные

Самое заметное изменение — параметры маршрутов теперь асинхронные. Каждый компонент, который раньше просто получал params или searchParams, теперь должен их await'ить. Это затрагивает страницы, layout'ы, обработчики маршрутов и даже специальные функции генерации изображений (opengraph-image, twitter-image, icon, apple-icon).

На первый взгляд паттерн выглядит просто:

1
2
3
4
5
6
7
8
9
// Next.js 15 - синхронные параметры
export default function Page({ params }) {
  const { slug } = params
}

// Next.js 16 - асинхронные параметры
export default async function Page({ params }) {
  const { slug } = await params
}

Но это касается не только страниц. Layout'ы тоже нуждаются в таком же подходе. В обработчиках маршрутов segmentData.params теперь промис. Даже функции generateMetadata требуют await для params и searchParams перед их использованием.

Функции генерации изображений тоже затронуты. При использовании generateImageMetadata и params, и id являются промисами:

// Next.js 16 - генерация изображений
export async function generateImageMetadata({ params }) {
    const { slug } = await params;
    return [{ id: '1' }, { id: '2' }];
}

export default async function Image({ params, id }) {
    const { slug } = await params;
    const imageId = await id; // id теперь Promise<string>
}

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

Это особенно важно, когда параметры приходят из разных источников. Да, slug извлекается из URL. Но иногда параметры маршрута требуют валидации в базе данных, разрешения edge-конфигурации или сетевых запросов. Асинхронный паттерн даёт Next.js возможность оптимизировать всё это без блокировки всего конвейера рендеринга.

Если вы используете TypeScript, готовьтесь к обновлению типов. Вместо Params = { slug: string } теперь пишете Params = Promise<{ slug: string }>. Это распространяется везде, где используются параметры, что в большом приложении может затронуть... много файлов.

Для клиентских компонентов, которым нужны параметры маршрута, хук use из React позволяет развернуть промис:

1
2
3
4
5
6
7
'use client'
import { use } from 'react'

export default function ClientPage(props: { params: Promise<{ slug: string }> }) {
  const params = use(props.params)
  const slug = params.slug
}

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

Turbopack становится бандлером по умолчанию

Turbopack разрабатывался годами. Vercel перестроила бандлер с нуля на Rust, и в Next.js 16 он наконец стал дефолтным. Никаких флагов, никакой экспериментальной конфигурации. Запускаете next dev или next build — и используете Turbopack.

Переход плавный. Уберите флаг --turbopack из скриптов в package.json. Если было "dev": "next dev --turbopack", просто сделайте "dev": "next dev". То же для команд сборки.

Разница в производительности реальна, особенно на больших приложениях. Холодный старт быстрее при запуске dev. Hot module replacement ощущается мгновенным. Продакшен-сборки тоже ускоряются, и выигрыш масштабируется с количеством модулей. Если работаете над чем-то с тысячами модулей, вы заметите.

По конфигурации Turbopack переехал из экспериментальной секции:

// Next.js 15 - экспериментальная конфигурация
const nextConfig = {
    experimental: {
        turbopack: {
            // опции
        },
    },
};

// Next.js 16 - стабильная конфигурация
const nextConfig = {
    turbopack: {
        // опции
    },
};

Есть новая функция кеширования файловой системы, о которой стоит знать. Включите experimental.turbopackFileSystemCacheForDev или experimental.turbopackFileSystemCacheForBuild, и Turbopack сохранит артефакты компиляции в .next между запусками. Перезапустите dev-сервер или запустите ещё одну сборку — и она будет намного быстрее. В CI/CD, если вы кешируете директорию .next, сборки значительно ускорятся.

1
2
3
4
5
6
const nextConfig = {
    experimental: {
        turbopackFileSystemCacheForDev: true,
        turbopackFileSystemCacheForBuild: true,
    },
};

Трейсинг тоже улучшился. Запустите npx next internal trace .next/dev/trace-turbopack и получите детальные трейсы, показывающие, куда уходит время компиляции. Супер полезно при отладке медленных сборок.

Если вам нужен Webpack для чего-то конкретного, есть opt-out. Добавьте --webpack к команде сборки: "build": "next build --webpack". Dev всё ещё использует Turbopack, но у вас есть запасной вариант.

Легаси-паттерны Webpack могут потребовать доработки. Например, импорты Sass с тильда-префиксом (~). Это была фишка Webpack. Turbopack не поддерживает это из коробки, но можно настроить resolve alias:

1
2
3
4
5
6
7
const nextConfig = {
    turbopack: {
        resolveAlias: {
            '~*': '*',
        },
    },
};

То же, если клиентский код случайно импортирует Node-модули вроде fs. Можно настроить fallback'и, чтобы избежать ошибок сборки. Лучше исправить импорты, но resolveAlias поможет пережить миграцию.

Поддержка React Compiler

С React 19 React Compiler стал стабильным. Next.js 16 поддерживает его как продакшен-готовую функцию.

Компилятор автоматически мемоизирует компоненты, чтобы сократить ненужные ре-рендеры. Меньше времени на оборачивание всего в useCallback и useMemo.

Включается так:

1
2
3
const nextConfig = {
    reactCompiler: true,
};

Он стабилен, но opt-in. Вы контролируете, когда его включать. Насколько он поможет, зависит от приложения. Компоненты со сложными паттернами ре-рендеринга получат наибольший выигрыш.

Улучшенные API кеширования

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

Эти API потеряли префикс unstable_ и стали продакшен-готовыми. Сдвиг от автоматического, неявного кеширования к явному, контролируемому разработчиком.

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

cacheTag даёт систему тегирования. Получаете данные, тегируете их. Позже, когда данные меняются, инвалидируете по тегу вместо обнуления всего. Пользователь обновил профиль? Инвалидируйте тег user-123, а не все данные пользователей.

updateTag решает проблему "read-your-writes". Пользователь отправляет форму, обновляет профиль, хочет сразу увидеть изменение. Без updateTag он может получить устаревший кеш. updateTag истекает тег и получает свежие данные в том же запросе. Пользователь видит свои изменения мгновенно:

1
2
3
4
5
6
7
8
'use server'
import { updateTag } from 'next/cache'

export async function updateUserProfile(userId: string, profile: Profile) {
  await db.users.update(userId, profile)
  // Истекаем кеш и обновляем сразу — пользователь видит изменения немедленно
  updateTag(`user-${userId}`)
}

В чём разница между updateTag и revalidateTag? revalidateTag помечает данные как устаревшие. Пользователи видят старые данные, пока новые загружаются в фоне. Хорошо для статей или контента, где устаревание допустимо. updateTag блокируется до прихода свежих данных. Мгновенная видимость.

refresh запускает обновление клиентского роутера из серверных экшенов. Изменяете данные на сервере, говорите клиенту обновить затронутый UI:

1
2
3
4
5
6
7
8
'use server'
import { refresh } from 'next/cache'

export async function markNotificationAsRead(notificationId: string) {
  await db.notifications.markAsRead(notificationId)
  // Обновляем счётчик уведомлений в хедере
  refresh()
}

Импорты стали чище:

1
2
3
4
5
6
7
8
// Next.js 15 - экспериментальные с алиасами
import {
    unstable_cacheLife as cacheLife,
    unstable_cacheTag as cacheTag,
} from 'next/cache';

// Next.js 16 - стабильные, чистые импорты
import { cacheLife, cacheTag, updateTag, refresh } from 'next/cache';

Также experimental.dynamicIO переименован в cacheComponents и стал стабильным. Связан с Partial Pre-Rendering (PPR):

// Next.js 15
const nextConfig = {
    experimental: {
        dynamicIO: true,
    },
};

// Next.js 16
const nextConfig = {
    cacheComponents: true,
};

unstable_cache всё ещё существует (с префиксом unstable) для оборачивания асинхронных функций с кешированием. Настраивается тегами и ревалидацией:

import { unstable_cache } from 'next/cache';

const getCachedUser = unstable_cache(
    async () => getUserById(userId),
    [userId],
    {
        tags: ['user'],
        revalidate: 3600, // Кешировать на 1 час
    },
);

Всё это согласуется с движением Next.js к React Server Components и стримингу. Когда вы явно объявляете поведение кеша, фреймворк знает, когда получать данные, стримить или отдавать из кеша.

Middleware переименован в Proxy

Middleware теперь называется proxy. Почему? "Middleware" означает разные вещи в разных фреймворках. "Proxy" понятнее. Он находится между запросом и приложением, проксируя и модифицируя запросы перед попаданием в маршруты.

Миграция простая. Переименуйте файл:

mv middleware.ts proxy.ts

Обновите экспорты:

1
2
3
4
5
// Next.js 15
export function middleware(request: Request) {}

// Next.js 16
export function proxy(request: Request) {}

Более значительное изменение: proxy-функции теперь работают только на Node.js runtime. Поддержка Edge runtime убрана. Если вы полагались на Edge middleware для низколатентного перехвата запросов, это ломает вещи.

Почему убрали Edge? Поддержка двух рантаймов для одной функции была сложной. Edge имеет ограничения, которых нет у Node.js. Разные API, проблемы совместимости пакетов. Стандартизация на Node.js упрощает всё и уменьшает странные edge-кейсы.

Если вы использовали Edge middleware для геолокационной маршрутизации, A/B-тестирования или проверок авторизации близко к пользователям, есть варианты. Адаптируйте логику proxy для Node.js. Если вам действительно нужна близость Edge для латентности, вынесите эту логику за пределы Next.js. Vercel Edge Functions, Cloudflare Workers, что предлагает ваш хостинг.

Один паттерн, который стоит рассмотреть: используйте proxy-функции чисто для маршрутизации и rewrite'ов. Держите авторизацию и динамическую логику в серверных компонентах или обработчиках маршрутов. Вы получаете стриминг React Server Components, сохраняя границы безопасности.

Есть новая опция конфигурации, skipProxyUrlNormalize, которая контролирует нормализацию URL перед попаданием в proxy. Важно, если нужно сохранить специфичное форматирование URL или порядок query-параметров.

Proxy-функции больше не могут возвращать тела ответов. Раньше можно было возвращать полные ответы из middleware, JSON или HTML. Не в Next.js 16. Proxy строго для модификации запросов, rewrite'ов и редиректов. Кастомные ответы принадлежат обработчикам маршрутов или API routes.

Пример: auth middleware, который возвращал 401 JSON, теперь редиректит на логин:

// Старый паттерн - больше не поддерживается
export function middleware(request: Request) {
  if (!isAuthValid(request)) {
    return NextResponse.json({ message: 'Auth required' }, { status: 401 })
  }
  return NextResponse.next()
}

// Новый паттерн - редирект вместо этого
export function proxy(request: Request) {
  if (!isAuthValid(request)) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', request.nextUrl.pathname)
    return NextResponse.redirect(loginUrl)
  }
  return NextResponse.next()
}

Лучшее разделение ответственности. Перехват запросов остаётся в proxy. Генерация ответов остаётся в маршрутах.

Поддержка AMP удалена

AMP ушёл. Больше никакого хука useAmp, никакой конфигурации amp-страниц, никаких гибридных AMP/HTML страниц.

Почему? Популярность AMP снизилась, поскольку браузеры улучшились, а фреймворки вроде Next.js стали быстрее. Современный Next.js уже обеспечивает производительность, которую обещал AMP. Удаление упрощает фреймворк.

Если вы используете AMP, вам понадобятся альтернативы перед обновлением. Хорошие новости: встроенные оптимизации Next.js, вероятно, уже соответствуют или превосходят то, что давал AMP.

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

Обработка изображений в Next.js 16 получила усиленную безопасность и лучшую производительность. Изменения в том, как next/image обрабатывает и кеширует изображения, основаны на реальных данных и проблемах безопасности.

Локальные изображения с query-строками теперь требуют явной конфигурации. Раньше можно было делать <Image src="/assets/photo?v=1" /> без настройки. Это создавало вектор атаки для перечисления файловой системы. Теперь нужно объявить:

const nextConfig = {
    images: {
        localPatterns: [
            {
                pathname: '/assets/**',
                search: '?v=1',
            },
        ],
    },
};

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

Минимальный TTL кеша по умолчанию вырос с 60 секунд до 4 часов (14400 секунд). Оптимизация затрат. Ревалидация изображений дорогая — пропускная способность, обработка, затраты CDN. Большинство изображений не меняется достаточно быстро, чтобы оправдать ревалидацию каждую минуту. Когда Next.js обрабатывает внешнее изображение без явных заголовков Cache-Control, он предполагает более длительное время жизни кеша.

Нужен более короткий кеш? Задайте явно:

1
2
3
4
5
const nextConfig = {
    images: {
        minimumCacheTTL: 60, // Восстановить дефолт в 60 секунд
    },
};

Дефолты качества изображений изменились для исправления раздутия srcset. Раньше: несколько вариантов качества для адаптивных изображений. Теперь: один уровень качества ([75]) по умолчанию. Меньшие атрибуты srcset, меньше вариантов для обработки и кеширования. Для большинства приложений одного уровня качества достаточно. Нужно несколько (премиум vs. ограниченная пропускная способность)? Объявите их:

1
2
3
4
5
const nextConfig = {
    images: {
        qualities: [50, 75, 100],
    },
};

16px убран из массива imageSizes по умолчанию. Почему? Анализ использования показал, что 16px изображения почти все — фавиконы, которые не проходят через next/image. Меньше вариантов для адаптивных изображений. Нужно 16px? Добавьте обратно:

1
2
3
4
5
const nextConfig = {
    images: {
        imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    },
};

Удалённые изображения получили усиление безопасности. images.domains объявлен устаревшим. Используйте images.remotePatterns с явным протоколом:

// Устарело
const nextConfig = {
    images: {
        domains: ['example.com'],
    },
};

// Безопасно
const nextConfig = {
    images: {
        remotePatterns: [
            {
                protocol: 'https',
                hostname: 'example.com',
            },
        ],
    },
};

Предотвращает случайную загрузку по HTTP. Делает безопасность явной. Ограничивайте дальше по pathname и порту при необходимости.

images.maximumRedirects ограничивает HTTP-редиректы при получении изображений. Дефолт изменился с неограниченного на 3. Предотвращает DoS, где URL изображения редиректит бесконечно. Отключите редиректы (maximumRedirects: 0) или увеличьте для edge-кейсов.

Для разработки с локальными IP, images.dangerouslyAllowLocalIP — ваш обходной путь. Next.js блокирует оптимизацию изображений для локальных IP по умолчанию. Установите в true, чтобы разрешить. Префикс "dangerously" означает — используйте только в приватных сетях.

Runtime-конфигурация заменена переменными окружения

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

Миграция:

.env
NEXT_PUBLIC_API_URL="/api"  # Клиентская сторона
DATABASE_URL="postgres://..." # Только серверная сторона

Клиентские переменные нуждаются в префиксе NEXT_PUBLIC_. Серверные — нет. Вот и всё.

Изменения интеграции ESLint

Команда next lint удалена. Используйте ESLint напрямую.

Codemod для миграции:

npx @next/codemod@canary next-lint-to-eslint-cli .

Обновляет проект для использования ESLint напрямую. Опция eslint в next.config.js тоже убрана.

Стратегия миграции

Обновление до Next.js 16 требует осторожности. Breaking changes достаточно значительные, чтобы спешка дала вам runtime-ошибки или баги, которые проявятся только в продакшене.

Начните с автоматического codemod:

npx @next/codemod@canary upgrade latest

Это обрабатывает механические вещи автоматически. Обновляет package.json до Next.js 16 и React 19. Конвертирует синхронные params и searchParams в асинхронные, добавляя await и помечая функции async. Переименовывает experimental.dynamicIO в cacheComponents. Убирает префикс unstable_ из импортов кеша.

Codemod не идеален. Он работает на статическом анализе и не всегда может определить правильную трансформацию. Когда застревает, оставляет комментарии @next-codemod-error, показывающие, что нужно ручное ревью.

Частые ручные исправления: параметры, передаваемые через хелпер-функции, сложная деструктуризация, которую codemod не может распарсить, компоненты, экспортируемые/импортируемые между файлами, где типы недоступны, параметры, сохранённые в переменные и затем переданные другим функциям.

После codemod тестируйте систематически. Обработчики маршрутов — высокий приоритет — segmentData.params влияет на каждый обработчик с динамическими сегментами. Страничные компоненты с динамическими маршрутами или search params нуждаются в тестировании. Layout-компоненты, обращающиеся к параметрам, требуют проверки. Функции генерации метаданных теперь await'ят и params, и searchParams.

Изображения требуют внимания. Используете query-строки для локальных изображений? Добавьте конфигурацию localPatterns. Проверьте домены внешних изображений. Мигрируйте с domains на remotePatterns. Проверьте, что конфигурация кеширования изображений работает с новым дефолтным TTL в 4 часа.

Миграция middleware — больше, чем переименование. Проверьте, что логика работает на Node.js runtime, если использовали Edge. Middleware, возвращавший тела ответов, нуждается в рефакторинге на редиректы/rewrite'ы. Тестируйте auth-потоки тщательно — тела ответов на редиректы меняют UX.

Переменные окружения критичны, если использовали serverRuntimeConfig или publicRuntimeConfig. Серверные значения в env-переменных без префикса NEXT_PUBLIC_. Клиентские значения с префиксом NEXT_PUBLIC_ и доступные в клиентских компонентах.

Проекты на TypeScript имеют обширные изменения типов. Обновите везде, где ссылались на params или searchParams. { slug: string } становится Promise<{ slug: string }> везде. Запустите tsc --noEmit, чтобы поймать ошибки типов до runtime.

Поведение dev vs. production может отличаться. Всегда тестируйте production-сборки локально перед деплоем. Запустите npm run build, затем npm start. Проверьте поведение кеширования — оно отличается между dev и production.

Параллельные маршруты получили breaking change, который легко пропустить: пустые слоты теперь требуют default.js. Используете параллельные маршруты с потенциально пустыми слотами? Создайте файлы default.js, которые возвращают null или вызывают notFound():

1
2
3
4
// app/@slot/default.tsx
export default function Default() {
    return null;
}

Поведение прокрутки изменилось незаметно. Next.js 16 больше не переопределяет CSS scroll-behavior: smooth во время навигации. Если полагались на старое поведение (Next.js временно устанавливал scroll в auto), добавьте data-scroll-behavior="smooth" к корневому HTML:

1
2
3
4
5
6
7
export default function RootLayout({ children }) {
    return (
        <html lang="en" data-scroll-behavior="smooth">
            <body>{children}</body>
        </html>
    );
}

CI/CD-пайплайны могут потребовать настройки. Кешируете .next между сборками? Структура директории изменилась. Новая директория .next/dev позволяет одновременные dev и build. Обновите конфигурацию кеша, чтобы включить её. Используете кеширование файловой системы Turbopack в CI? Кешируйте всю директорию .next.

Build-адаптеры достигли alpha с экспериментальным API. Работаете над кастомными целями деплоя или нужно хукнуться в процесс сборки? experimental.adapterPath указывает на модуль кастомного адаптера. Продвинутая функция для хостинг-провайдеров и авторов фреймворков.

Мониторинг ошибок может показать новые паттерны от асинхронных параметров. Убедитесь, что отслеживание ошибок захватывает полные асинхронные стек-трейсы. Некоторые error boundary могут потребовать корректировки, если они ловили ошибки от синхронного доступа к параметрам.

Для полных деталей смотрите официальный анонс Next.js 16.

Источник: https://www.trevorlasn.com/blog/whats-new-in-nextjs-16