Миграция на Next.js 16: Turbopack по умолчанию и замена middleware

Что ломается при переходе на Next.js 16: Turbopack как дефолт, депрекейт middleware, изменения кеша. Кому обновляться сейчас, а кому ждать.

Миграция на Next.js 16: Turbopack по умолчанию и замена middleware

Миграция на Next.js 16: Turbopack по умолчанию и замена middleware

Недавно вышла Next.js 16. Всю неделю Vercel отчитывалась про «самый плавный мажорный релиз за последние годы» в своём блоге, а Discord фреймворка разрывался от обсуждений: у Cal.com что-то отвалилось в проде, пара SaaS-стартапов с аутентификацией мучались, и многих удивило, что middleware.ts теперь поднимает warning при сборке и считается устаревшим.

Короче, Vercel сделала то, о чём предупреждала ещё осенью в RFC. Turbopack стал сборщиком по умолчанию для next dev и next build. Webpack остался, но его нужно включать через флаг --webpack, и документация прямо рекомендует переезжать на Turbopack. А middleware.ts переименовали в proxy.ts — сам файл и экспортируемую функцию, — и важная деталь: proxy работает только на Node.js, Edge Runtime в нём не поддерживается. Ну и ещё десяток мелочей, которые по отдельности вроде бы ничего, а в сумме у некоторых команд привели к проблемам в проде на 4-6 часов.

Разберём по делу: что конкретно сломалось, кому стоит обновиться прямо сейчас, а кто может подождать хотя бы до 16.2.

Turbopack как дефолт — что это значит на самом деле

Vercel уже лет пять говорит про Turbopack. Сначала как про «Webpack-killer на Rust», потом как про «бету для разработки», потом как про «стабильную версию для разработки». И вот, наконец, в 16-й версии он стал сборщиком по умолчанию и для production.

У нас на среднем проекте (~300 страниц) холодный старт next dev упал с ~10 секунд до ~2 — в русле бенчмарков Vercel. HMR стал почти мгновенным, особенно на больших страницах с тяжёлыми клиентскими компонентами. Сборка production-бандла на проектах в 200-500 страниц происходит быстрее в 2-4 раза. Цифры звучат неплохо, и они правдивы — у большинства команд всё именно так.

Только вот деталь: Turbopack в 16.0 поддерживает примерно 94% Webpack-конфигов «из коробки». «Почти все» — это хорошо, но оставшиеся 6% часто оказываются самыми неприятными, особенно кастомные лоадеры. SVG-инлайнеры с runtime-обработкой, цепочки MDX с собственными плагинами remark, самодельные resolver-алиасы, генерация типов через babel-plugin… Если у вас в next.config.js есть хоть один webpack: (config) => {...} с нетривиальной логикой — ждите сюрпризов.

Вот пара примеров из жизни, с чем столкнулись:

SVGR. Если вы импортировали SVG как React-компонент через @svgr/webpack, то в Turbopack это работает по-другому — нужен turbopack.rules в конфигурации. Старый лоадер просто игнорируется без ошибок. Импорты возвращают пустой объект, страница рендерится, кнопок нет, и всем становится непонятно, что происходит.

Sentry и подобные инструменты для source-map. Старые версии Sentry SDK не умели генерировать production source-maps под Turbopack, и маппинг ошибок превращался в кашу из минифицированных имён. В свежих версиях @sentry/nextjs это поправили. Если у вас в package.json зафиксирована старая версия — обновите её до актуальной, иначе сборка, возможно, пройдёт успешно, но трейсы в Sentry читать будет невозможно.

Кастомные babel-конфиги. Тут как раз приятный сюрприз: вопреки распространённому мнению, Turbopack в 16-й версии умеет работать с Babel. Если он находит ваш babel-конфиг (например, для babel-plugin-react-compiler или babel-plugin-styled-components), то сам включает Babel-проход. Платой будет скорость — Babel ощутимо медленнее SWC, и сборка с включённым React Compiler заметно дольше, об этом честно пишут в документации. Так что конфиг не «перестаёт читаться», но если можете заменить плагин на нативный SWC-аналог — выиграете в скорости.

Что делать? Прежде чем обновляться, запустите next build --turbopack на отдельной ветке (этот флаг уже работал в 15.x), сравните результат со старой сборкой через --webpack, и проведите e2e-тесты. И главное — не верьте, что сборка прошла успешно. Если она выдала результат в Turbopack, это ещё не значит, что артефакт корректный.

Middleware → proxy — переименование, которое не такое безобидное, как кажется

Middleware появился еще в 12-й версии и быстро стал привычным местом для аутентификации, A/B-тестов, гео-редиректов и логики мультитенантности. У сотен тысяч проектов он лежит в корне как middleware.ts и работает на Edge Runtime.

В 16-й версии его не выпиливают и не заменяют какой-то новой моделью «логики по сегментам». Происходит более скучная, но коварная вещь: middleware.ts переименовали в proxy.ts. Меняется имя файла и имя экспортируемой функции — middleware становится proxy. Сама логика остаётся ровно той же: это по-прежнему ОДИН корневой файл, который перехватывает запросы до роутинга, matcher никуда не делся, API запроса/ответа тот же. Старое имя пока работает, но при сборке поднимает warning и помечено как устаревшее.

# По сути вся миграция выглядит так:
mv middleware.ts proxy.ts
// proxy.ts
export function proxy(request: Request) {
 // та же логика, что была в middleware
}

Есть даже кодмод, который сделает это за вас (npx @next/codemod@canary upgrade latest) и заодно переименует связанные флаги конфига — например, skipMiddlewareUrlNormalize превращается в skipProxyUrlNormalize.

Но есть один пункт, из-за которого «просто переименуй файл» иногда оборачивается несколькими часами в проде. proxy работает только на Node.js. Edge Runtime в нём не поддерживается, и рантайм нельзя сконфигурировать — он всегда nodejs.

Раньше Middleware жил на Edge Runtime — облегченной версии Node.js на основе V8 isolates. Это давало минимальную задержку на границе CDN, но создавало кучу проблем: нельзя было использовать половину npm-пакетов (нет fs, нет нативных модулей, ограничение на размер бандла, странности с криптографией). Команды постоянно натыкались на эти ограничения, когда требовалось сделать что-то сложнее простого редиректа. Vercel переименованием как раз и подчёркивает суть: это сетевой прокси перед приложением, и он переезжает на полноценный Node.js.

В чём подвох на практике?

  1. Если вы опирались именно на Edge Runtime — переезд не бесплатный. Код, который раньше крутился на edge с его минимальной задержкой, теперь поедет на Node.js. Для большинства это нормально (а часто и удобнее — наконец-то можно тащить любые npm-пакеты), но если вы сознательно выбирали edge ради задержки в 5-15 мс на границе CDN, профиль латентности изменится, и это надо замерить. Тем, кому edge критичен, документация пока разрешает остаться на старом middleware — обещают отдельные инструкции по edge в минорном релизе.

  2. Это всё ещё единая точка входа, и в этом плюс. Не верьте слухам про «теперь логику надо разносить по сегментам App Router» — ничего подобного, ваш условный middleware на 200 строк с проверкой JWT, гео-cookie, rewrite для тенанта и логом переезжает целиком в proxy.ts как есть. Переписывать архитектуру не нужно, нужно поменять имя файла, имя функции и проверить, что вся логика жива на Node.js-рантайме.

  3. matcher на месте. Старый декларативный матчинг (/dashboard/:path* одной регуляркой) продолжает работать так же. Никакого нового способа фильтровать роуты учить не надо.

  4. Гео и прочие request-данные. Тут стоит держать в голову, что async Request APIs (headers(), cookies()) в 16-й окончательно стали асинхронными — синхронный доступ убрали. А request.geo/request.ip в middleware Vercel убрал ещё в 15-й версии (нужно читать гео из заголовков), так что если вы аккуратно обновлялись через 15-ю, в проксе ничего нового тут не появится. Если же прыгаете с более старой версии — проверьте, что A/B-тест на гео всё ещё читает заголовки, а не несуществующий request.geo, иначе все попадут в контрольную группу и метрики просядут.

На нашем опыте обучения фронтенд-разработчиков, переезд с middleware на proxy — это первая задача, на которой джуниоры начинают понимать, что фреймворк не статичен. Многие приходят с мыслью «выучу Next.js и буду работать», а на этом переименовании впервые упираются в то, что каждые 12 месяцев тут меняется что-то важное — пусть даже это всего лишь имя файла.

Что ещё ломается, о чём пишут меньше

Кроме двух главных проблем, в 16-й версии есть ещё пара изменений, которые тоже могут вызвать неожиданности.

React 19.2 как минимум. Если вы сидели на React 18.3 и использовали use() с фоллбэками в App Router или у вас были экспериментальные forwardRef-композиции, то поведение Compiler в 19.2 стало строже. У некоторых проектов появились warning, которые в режиме разработки превращаются в ошибки. Не критично, но неприятно.

images.domains помечен как устаревший. Старый способ разрешать внешние домены через images.domains: [...] в 16-й официально deprecated — его пока не удалили, но Vercel настойчиво двигает на images.remotePatterns, который безопаснее (можно ограничить протокол, хост и путь). Если у вас всё ещё domains, увидите warning при сборке; перепишите на remotePatterns, пока его действительно не вырезали в одном из следующих мажоров. Заодно в 16-й подкрутили дефолты next/image (например, qualities теперь по умолчанию только [75], а minimumCacheTTL вырос до 4 часов) — если опирались на старые значения, проверьте.

getServerSideProps в Pages Router поднял warning. Pages Router пока ещё жив, но Vercel явно направляет нас в другое русло. Если у вас гибрид (Pages + App Router) — пора планировать миграцию на App Router, потому что в 17-й версии Pages Router, скорее всего, тоже устареет.

Изменения в кешировании. В 15-й версии Vercel сильно переработал семантику fetch-кеша, а в 16-й еще немного подкрутил. Например, dynamic = 'force-dynamic' теперь не отключает Data Cache для вложенных fetch, нужно явно указать cache: 'no-store' для каждого запроса. У команд, у которых SSR работает с персональными данными, внезапно оказалось, что один пользователь видит данные другого. Такое уже случалось как минимум у двух команд, которые публично описывали свои проблемы.

Turbopack и monorepo. В pnpm-workspaces с symlinked-пакетами Turbopack в 16.0 иногда не подхватывает изменения в дочерних пакетах при HMR. Это известный баг, который обещают исправить в 16.1. Если вы используете Turborepo или Nx — проверьте это отдельно.

Кому обновляться сейчас, а кому подождать

Это не лозунг «обновляемся все срочно!». Картинка зависит от типа вашего проекта.

Обновляться сейчас имеет смысл, если:

Стоит подождать минимум до 16.2 (обычно выходит через 2-3 месяца), если:

Обновляться не стоит вообще, если:

Что это говорит про Next.js вообще?

За пять лет Next.js вырос из простого SSR для React в полноценный фреймворк с собственным сборщиком, рантаймом, моделью кеширования и сетевым слоем. Это не плохо, но это означает, что зависимость от решений Vercel стала глубже, чем от Express или даже Remix (который сейчас, кстати, переименовали в React Router v7, и многие фронтенд-разработчики смотрят на него в поисках альтернативы, уставая от темпов изменений в Next.js).

Новые разработчики часто спрашивают: «Next.js — это надолго или скоро устареет?». Честный ответ: и то, и другое. База пользователей и поддержка Vercel никуда не денутся, так что жить фреймворку ещё долго. Но «выучил один раз и забыл» с ним не выйдет.

Если вы только начинаете свой путь во фронтенде, учите App Router и React Server Components. Не тратьте время на устаревшие части вроде Pages Router и Edge Middleware. Хотя их знание тоже может пригодиться — на legacy-проектах они еще долго будут жить — но лучше сосредоточиться на том, куда движется фреймворк.

Если вы уже работаете с Next.js, не обновляйтесь в первый день и заведите привычку вычитывать раздел deprecations в каждом релизе — почти все сюрпризы в проде растут именно из проигнорированного warning'а.

А middleware, пожалуй, можно и похоронить. API был неплохой. Просто Edge Runtime оказался компромиссом, на который разработчики натыкались слишком часто.

Хочешь войти в IT за 12 месяцев?

Выбери трек — Python, Flutter или QA — и начни с бесплатного вводного урока

Выбрать трек