Next.js v15 — Работа над Ошибками
- среда, 23 октября 2024 г. в 00:00:04
Привет! Это ставшая уже регулярной рубрика о релизах next.js (читайте релизы по v11, v12, v13, v14). Каждый релиз - набор нового, интересного и спорного. Новая версия не станет исключением. Но всё же новая версия интересна не столько новым функционалом, сколько изменением приоритетов и организации в next.js. И да, как вы уже догадались из названия, в значительной части релиз ценен проработкой ошибок и доработками.
Я работаю с next.js примерно с 8-й версии. Всё это время с интересом наблюдаю за его развитием (порою не без разочарования). В последнее время я выпустил ряд статей про борьбу с новым App Router-ом - “Next.js App Router. Путь в будущее или поворот не туда”, “Кеширование next.js. Дар или проклятие”, “Больше библиотек богу библиотек или как я переосмыслил i18n”. Все они стали следствием очень слабой проработки идей и возможностей в предыдущих версиях next.js. А от этого интерес к новой версии только вырос. Вместе с тем и желание понять вектор изменений фреймворка.
В данной статье я не буду останавливаться на том что такое App Router или серверные компоненты - про это подробно расписано в предыдущих статьях. Только про новую версию и только про новые изменения.
Примечание: в статье отражены самые интересные изменения с призмы автора. Презентация новой версии состоится 24 октября. О ней будет выпущена дополнительная статья.
Сперва немного об изменениях во внутренних процессах разработки next.js. Команда фреймворка впервые опубликовала кандидат на релиз (RC версию). Очевидно, что сделали они это из-за решения команды React.js опубликовать именно React v19 RC.
Обычно команда next.js в своих стабильных релизах спокойно использует у себя react из релизной ветки “Canary” (эта ветка считается стабильной и рекомендуется для использования фреймворками). В этот же раз они решили поступить иначе (забегая в будущее - не зря).
План обеих команд был прост - опубликовать предрелизную версию, дать сообществу проверить на проблемы и через пару недель опубликовать полноценный релиз.
С релиза релиз-кандидата React.js прошло более полугода, но стабильная версия всё ещё не опубликована. Отсрочка релиза стабильной версии React.js ударила и по планам next.js. Поэтому, вопреки традициям, они опубликовали целых 15 дополнительных миноров когда уже во всю собирали 15-ю версию (обычно 3-5 миноров и релиз). Примечательно здесь то, что эти миноры включали не все накопленные изменения, а только исправление критичных моментов, что также выбивается из привычных процессов next.js.
Базовый порядок выкладки в next.js - всё сливается в ветку canary, а потом, в какой-то момент, эта ветка публикуется как стабильный релиз.
Однако, в результате команда next.js решила отвязаться от релиза React.js и опубликовать стабильную версию фреймворка до публикации стабильной версии React.js.
Ещё одно очень полезное организационное изменение. Наконец можно смотреть разные версии документации. Собственно, почему это так важно.
Во-первых обновление next.js в связи с крупными изменениями часто бывает достаточно сложной задачей. В общем-то по этой причине всё ещё более 2млн загрузок у 12-й версии и более 4млн у 13-й (справедливости ради, у 14-й версии более 20млн загрузок) ежемесячно.
Соответственно пользователям предыдущих версий нужна документация именно их версии, т.к. новая может быть переписана на половину.
Ещё одна проблема в том, что next.js по сути использует единый канал. В него же вносятся и изменения в документацию. Поэтому описания изменений из canary версий сразу появлялись в основной документации. Теперь же они отображаются под разделом “canary”.
В начале я упомянул, что next.js сейчас использует RC версию React.js. Но на самом деле это не так, точнее не совсем так. По факту next.js сейчас использует две конфигурации React.js: 19-ю canary версию для App Router и 18-ю версию для Pages Router.
Интересно, что в один момент хотели включить 19-ю версию и для Pages Router, но затем эти изменения откатили. Сейчас же полноценную поддержку 19-й версии React.js обещают после релиза его стабильной версии.
Вместе с тем в новой версии будет несколько полезных улучшений для серверных действий функций (да-да, команда React их переименовала):
Оптимизация веса и производительности;
Улучшенная обработка ошибок;
Исправлены ревалидация и редиректы из серверных функций.
Пожалуй в этот же раздел отнесу нововведение next.js - компонент Form. В целом это уже знакомый нам form из react-dom, но с небольшими улучшениями. Этот компонент нужен в первую очередь если успешная отправка формы предполагает переход на другую страницу. Для следующей страницы будут предзагружены абстракции loading.tsx и layout.tsx.
import Form from 'next/form'
export default function Page() {
return (
<Form action="/search">
{/* On submission, the input value will be appended to
the URL, e.g. /search?query=abc */}
<input name="query" />
<button type="submit">Submit</button>
</Form>
)
}
Говоря о next.js нельзя не упомянуть разработческий опыт. Помимо стандартного “Быстрее, выше, сильнее” (о чём мы тоже поговорим, но немного позже), вышло несколько полезных улучшений.
Долгожданная поддержка ESlint v9. Next.js так и не поддерживал ESlint v9. Это при том, что и сам eslint (v8) и часть его подзависимостей уже помечены как deprecated. Из-за этого получалась неприятная ситуация, что проекты по сути были вынуждены держать deprecated пакеты.
Немного улучшился интерфейс ошибок (который в next.js понятный и удобный):
Добавлена кнопка копирования стека вызова;
Добавлена возможность открытие источника ошибки в редакторе на конкретной строке.
Добавлен “Static Indicator” - элемент в углу страницы показывающий что страница собрана в статическом режиме. В целом мелочь, но забавно что его включили в ключевые изменения как что-то новое. Индикатор “предсобранной” страницы был примерно так с 8-й версии (2019-го года) и здесь, по сути, просто незначительно обновили его и адаптировали для App Router.
Также добавлен каталог с отладочной информацией - .next/diagnostics. В нём можно будет найти информацию о процессе сборки и всех возникающих ошибках. Пока непонятно, будет ли это полезно в ежедневном использовании, но точно будет использоваться при поиске проблем при разборе с деврелами Vercel (да, они иногда помогают разобрать проблемы).
Следом после DX стоит поговорить и про сборку. А вместе с ней и про Turbopack.
И самая главная новость в этом. Turbopack полностью завершён для режима разработки! “100% существующих тестов выполнились без ошибок с turbopack”
Теперь же команда turbo работает над версией под продакшен, постепенно проходя по тестам и прорабатывая их (на данный момент около 96%)
Turbopack также добавляет и новые возможности:
Установка лимита памяти на сборку с turbopack;
Tree Shaking (удаление неиспользуемого кода).
const nextConfig = {
experimental: {
turbo: {
treeShaking: true,
memoryLimit: 1024 * 1024 * 512 // in bytes / 512MB
},
},
}
Эти и другие улучшения в turbopack “уменьшили использование памяти на 25-30%”, а также “ускорили сборку тяжёлых страниц на 30-50%”.
Исправлены значительные проблемы со стилями. В 14-й версии часто возникали ситуации, что у стилей при навигации ломался порядок и от этого стиль А становился то выше стиля Б, то ниже. От этого менялся их приоритет и соответственно элементы выглядели иначе.
Следующее долгожданное улучшение. Теперь файл конфигурации можно писать на TypeScript - next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
Ещё одно интересное нововведение - повторные попытки сборки статических страниц. То есть если страница не сможет собраться (например из-за проблем с интернетом) - она попробует собраться вновь.
const nextConfig = {
experimental: {
staticGenerationRetryCount: 3,
},
}
И в завершении раздела очень желанный сообществом функционал - возможность указывать путь до дополнительных файлов для сборки. С этой опцией можно, например, указать что файлы лежат не в директории app
, а в директориях modules/main, modules/invoces.
Однако на данный момент его добавили только под внутренние цели команды. И в этой версии его однозначно не презентуют. Дальше же оно либо будет использоваться под задачи Vercel, либо его протестируют и представят уже к следующему релизу.
Самая болезненная часть обновлений next.js. И в этой версии также есть критические обновления.
Ряд внутренних API фреймворка стали асинхронными - cookies
, headers
, params
и searchParams
(так называемые Динамические API).
import { cookies } from 'next/headers';
export async function AdminPanel() {
const cookieStore = await cookies();
const token = cookieStore.get('token');
// ...
}
Крупное изменение, но команда Next.js обещает что весь этот функционал сможет обновиться автоматически вызовом их codemod:
npx @next/codemod@canary next-async-request-api .
Ещё одно изменение, но вероятно мало к кому относящееся. Из NextRequest
(используется в middleware и API роутах) удалены ключи geo
и ip
. По сути этот функционал работал только в Vercel, в остальных же местах разработчики делали свои методы. Для Vercel же этот функционал будет вынесен в пакет @vercel/functions
И ещё несколько обновлений:
В revalidateTag
можно передавать сразу несколько тагов;
В конфигурацию под next/image
добавлены ключи images.remotePatterns.search
и images.localPatterns
. С их помощью можно лучше контролировать ограничения адресов по которым будет работать сжатие картинок.
const nextConfig = {
images: {
localPatterns: [
{
pathname: '/assets/images/**',
search: 'v=1',
},
],
},
}
По моему личному мнению именно здесь произошли самые важные изменения для next.js. И самая главная новость - Кэширование теперь по умолчанию отключено! Я не буду подробно останавливаться на проблемах кэширования, этому в значительной степени была посвящена статья “Кеширование next.js. Дар или проклятие”.
Пройдём по всем основным изменениям в кэшировании:
Собственно, fetch
по умолчанию использует значение no-store вместо force-cache;
API роуты по умолчанию работают в режиме force-dynamic (раньше по умолчанию force-static, то есть собирались в статический ответ во время сборки [если на странице не использовались динамические API]);
Отключено и кэширование в клиентском роутере. Раньше если клиент в рамках пути зашёл на страницу - она у него кэшировалась на клиенте и оставалась в таком состоянии до перезагрузки страницы. Теперь каждый раз будет загружаться актуальная страница. Перенастроить этот функционал можно через next.config.js
:
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30 // defaults to 0
},
},
}
При этом даже если клиентское кэширование будет включено - он, по всей видимости будет обновляться в нужный момент. А именно, если у включённого кэша страницы на сервере истечёт срок действия.
Серверные компоненты теперь кэшируются в режиме разработки. За счёт этого обновления в разработке происходят быстрее. Сбросить кэш можно просто перезагрузкой страницы. Также можно полностью отключить функционал через next.config.js
:
const nextConfig = {
experimental: {
serverComponentsHmrCache: false, // defaults to true
},
}
Можно управлять заголовком “Cache-Control”. Прежде оно жёстко перезатиралось всегда на внутренние значения next.js. От этого были артефакты с кешированием через CDN;
next/dynamic кэширует модули и переиспользует их, а не грузит каждый раз повторно;
Это что касается “исторических недоразумений”. Также в next.js появятся и новые API. А именно так называемое Dynamic I/O
. О нём ещё нигде не писали, поэтому дальше будут догадки автора исходя из изменений.
Dynamic I/O
судя по всему является продвинутым режимом динамической сборки. Чем-то вроде PPR (частичный пререндеринг), точнее его дополнением. Если коротко Partial Prerindering - это режим сборки страницы, при котором большинство элементов собираются на этапе сборки и кэшируются, а отдельные элементы собираются по каждому запросу.
Так вот, dynamic I/O
[вероятно] финализирует архитектуру под эту логику. Он расширяет возможности кеширования так, чтобы можно было включать и отключать его точечно в зависимости от режима и места использования (в “динамическом” блоке или нет).
const nextConfig = {
experimental: {
dynamicIO: true, // defaults to false
},
}
Вместе с тем добавляется директива "use cache"
. Она будет доступна в nodejs и edge рантаймах и, судя по всему, во всех серверных сегментах и абстракциях. Указав эту директиву вверху функции или модуля с экспортом функции - её результат будет закэширован. Директива будет доступна только при включённом dynamicIO
.
async function loadAndFormatData(page) {
"use cache"
...
}
Также специально под use cache
добавляются методы cacheLife
и cacheTag
export { unstable_cacheLife } from 'next/cache'
export { unstable_cacheTag } from 'next/cache'
async function loadAndFormatData(page) {
"use cache"
unstable_cacheLife('frequent');
// or
unstable_cacheTag(page, 'pages');
...
}
cacheTag
будет использоваться для ревалидации с помощью revalidateTag
, а cacheLife
будет задавать время жизни кэша. При этом в качестве значения cacheLife
нужно будет использовать одно из преднастроенных значений. Несколько опций будут доступны из коробки ("seconds", "minutes", "hours", "days", "weeks", "max”), дополнительные можно прописать в next.config.js
:
const nextConfig = {
experimental: {
cacheLife?: {
[profile: string]: {
// How long the client can cache a value without checking with the server.
stale?: number
// How frequently you want the cache to refresh on the server.
// Stale values may be served while revalidating.
revalidate?: number
// In the worst case scenario, where you haven't had traffic in a while,
// how stale can a value be until you prefer deopting to dynamic.
// Must be longer than revalidate.
expire?: number
}
}
}
}
Вероятно, главный герой следующего релиза. Как уже говорилось, PPR это режим сборки страницы, при котором большинство элементов собираются на этапе сборки и кэшируются, а отдельные элементы собираются по каждому запросу. При этом предсобранная часть сразу отправляется на клиент, а остальные догружаются динамически.
Сам по себе функционал был представлен ещё полгода назад в релиз кандидате в качестве экспериментального API. Этот API оставят в таком качестве и как стабильное мы его, вероятно, увидим только в 16-й версии (что хорошо, нередко крупный функционал переходил в отряд стабильных за полгода-год).
Что касается изменений. Как уже говорилось, в первую очередь он обновил принципы работы. Однако с точки зрения использования PPR это почти никак не задело. Вместе с тем он получил несколько улучшений:
Прежде в конфиге был просто флаг, теперь же чтобы включить PPR нужно указывать incremental. Это, по-видимому, сделано чтобы сделать логику прозрачнее - контент может кэшироваться разработчиками даже в PPR и чтобы его обновить нужно вызвать revalidate методы.
const nextConfig = {
experimental: {
ppr: 'incremental',
},
}
Также прежде PPR запускался для всего проекта, теперь же его нужно включать для каждого сегмента (layout
или page
):
export const experimental_ppr = true
Ещё одно изменение - Partial Fallback Prerendering (PFPR). Именно за счёт этого улучшения предсобранная часть сразу отправляется на клиент, а остальные догружаются динамически. На месте динамических элементов в этом время показывается callback компонент.
import { Suspense } from "react"
import { StaticComponent, DynamicComponent } from "@/app/ui"
export const experimental_ppr = true
export default function Page() {
return {
<>
<StaticComponent />
<Suspense fallback={...}>
<DynamicComponent />
</Suspense>
</>
};
}
Instrumentation отмечено как стабильное API. Файл instrumentation
позволяет пользователям подключаться к жизненному циклу сервера Next.js. Работает над всем приложением (в т.ч. всеми сегментами Pages Router и App Router).
На данный момент instrumentation поддерживает хуки:
register
- вызывается один раз при инициализации next.js сервера. Может использоваться для интеграции с библиотеками наблюдения (OpenTelemetry, datalog) или для специфичных задач проекта.
onRequestError
- новый хук, вызывающийся при всех ошибках сервера. Может использоваться для интеграций с библиотеками отслеживания ошибок (Sentry).
export async function onRequestError(err, request, context) {
await fetch('https://...', {
method: 'POST',
body: JSON.stringify({ message: err.message, request, context }),
headers: { 'Content-Type': 'application/json' },
});
}
export async function register() {
// init your favorite observability provider SDK
}
Interceptor, он же route-level middleware. Является чем-то вроде полноценного [уже существующего] middleware, но, в отличии от последнего:
Может работать в node.js рантайме;
Работает на сервере (а значит имеет доступ к окружению и единому кэшу);
Может быть добавлен множество раз и наследуется во вложенности (примерно также как работал middleware когда он был в бета версии);
Работает в т.ч. для серверных функций.
При этом при создании interceptor-файла, все страницы ниже по дереву становятся динамическими.
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
const signInPathname = '/dashboard/sign-in';
export default async function intercept(request: NextRequest): Promise<void> {
// This will also seed React's cache, so that the session is already
// available when the `auth` function is called in server components.
const session = await auth();
if (!session && request.nextUrl.pathname !== signInPathname) {
redirect(signInPathname);
}
}
// lib/auth.ts
import { cache } from 'react';
export const auth = cache(async () => {
// read session cookie from `cookies()`
// use session cookie to read user from database
})
Если говорить о Vercel, то теперь middleware будет эффективен как первичная простая проверка на уровне CDN (тем самым например сразу возвращать редиректы если запрос не разрешён), а interceptor будут работать уже на сервере, делая полноценные проверки и сложные операции.
В self-host же, по-видимому, такое разделение будет менее эффективно (так как обе абстракции работают на сервере). Возможно будет достаточно использовать только interceptor.
Перезапись fetch, жёсткое кеширование, множество багов и игнорирование запросов сообщества. Команда next.js приняла ошибочные решения, поспешила с релизами, держала свои взгляды несмотря на сообщество. Почти год потребовался до осознания проблем. И только сейчас, наконец, есть ощущение что фреймворк вновь решает проблемы сообщества.
С другой же стороны другие фреймворки. Год назад, на презентации React.js, казалось что вот-вот все фреймворки станут вровень с next.js. React стал реже упоминать next.js как главный инструмент, фреймворки показывали готовящиеся системы сборки, поддержку серверных компонент и функций, ряд глобальных изменений и объединений. Прошло время, а все они по сути так и не дошли до этой точки.
Конечно же, финальные выводы можно будет делать спустя время, но пока складывается ощущение, что изменения в React.js вместо ожидаемого тогда уравнивания фреймворков - привели к ещё большему доминированию next.js и большему расхождению фреймворков (т.к. реализацию серверных компонент и действий оставили на усмотрение фреймворков).
В тоже время Open AI перешёл на Remix (”в меру его большей стабильности и удобства”):
И начали они, по-видимому, ещё до значительных изменений в next.js:
В общем, на следующих stateofjs и stackoverflow survey мы, скорее всего, увидим значительные перестановки.
Сама же конференция состоится уже послезавтра, 24 октября, в 19:00 по Москве, в прямой трансляции на сайте nextjs.org/conf и на YouTube. Доклады обещают быть интересными. Также будет интересно послушать про изменения от самой команды next.js (с примерами, анимациями и планами).
Кредиты
Примеры кода или их основы взяты из документации next.js, а также из коммитов, PR и ядра next.js;
Постскриптум
Если вам нужен инструмент генерации документации на основе MD файлов - посмотрите на robindoc.com, если вы работаете с next.js - возможно вы найдёте полезное в решениях nimpl.tech.