javascript

Next.js v15 — Работа над Ошибками

  • среда, 23 октября 2024 г. в 00:00:04
https://habr.com/ru/articles/852352/

Привет! Это ставшая уже регулярной рубрика о релизах 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 v15

Сперва немного об изменениях во внутренних процессах разработки next.js. Команда фреймворка впервые опубликовала кандидат на релиз (RC версию). Очевидно, что сделали они это из-за решения команды React.js опубликовать именно React v19 RC.

Обычно команда next.js в своих стабильных релизах спокойно использует у себя react из релизной ветки “Canary” (эта ветка считается стабильной и рекомендуется для использования фреймворками). В этот же раз они решили поступить иначе (забегая в будущее - не зря).

План обеих команд был прост - опубликовать предрелизную версию, дать сообществу проверить на проблемы и через пару недель опубликовать полноценный релиз.

Твит разработчика core-команды React.js https://x.com/acdlite/status/1797668537349328923
Твит разработчика core-команды React.js https://x.com/acdlite/status/1797668537349328923

С релиза релиз-кандидата 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 - nextjs.org/docs
Версионирование документации next.js - nextjs.org/docs

Ещё одна проблема в том, что next.js по сути использует единый канал. В него же вносятся и изменения в документацию. Поэтому описания изменений из canary версий сразу появлялись в основной документации. Теперь же они отображаются под разделом “canary”.

Использование React

В начале я упомянул, что 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>
  )
}

Разработческий опыт (DX)

Говоря о next.js нельзя не упомянуть разработческий опыт. Помимо стандартного “Быстрее, выше, сильнее” (о чём мы тоже поговорим, но немного позже), вышло несколько полезных улучшений.

Долгожданная поддержка ESlint v9. Next.js так и не поддерживал ESlint v9. Это при том, что и сам eslint (v8) и часть его подзависимостей уже помечены как deprecated. Из-за этого получалась неприятная ситуация, что проекты по сути были вынуждены держать deprecated пакеты.

Немного улучшился интерфейс ошибок (который в next.js понятный и удобный):

  • Добавлена кнопка копирования стека вызова;

  • Добавлена возможность открытие источника ошибки в редакторе на конкретной строке.

Пример копирования стэка ошибки в next.js
Пример копирования стэка ошибки в next.js

Добавлен “Static Indicator” - элемент в углу страницы показывающий что страница собрана в статическом режиме. В целом мелочь, но забавно что его включили в ключевые изменения как что-то новое. Индикатор “предсобранной” страницы был примерно так с 8-й версии (2019-го года) и здесь, по сути, просто незначительно обновили его и адаптировали для App Router.

Также добавлен каталог с отладочной информацией - .next/diagnostics. В нём можно будет найти информацию о процессе сборки и всех возникающих ошибках. Пока непонятно, будет ли это полезно в ежедневном использовании, но точно будет использоваться при поиске проблем при разборе с деврелами Vercel (да, они иногда помогают разобрать проблемы).

Ответ команды next.js на твит о медленной сборке проекта
Ответ команды next.js на твит о медленной сборке проекта

Изменения в сборке

Следом после DX стоит поговорить и про сборку. А вместе с ней и про Turbopack.

Turbopack

И самая главная новость в этом. Turbopack полностью завершён для режима разработки! “100% существующих тестов выполнились без ошибок с turbopack”

Теперь же команда turbo работает над версией под продакшен, постепенно проходя по тестам и прорабатывая их (на данный момент около 96%)

image.png
Пример участка changelog-а в next.js

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, либо его протестируют и представят уже к следующему релизу.

Изменение API фреймворка

Самая болезненная часть обновлений 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)

Вероятно, главный герой следующего релиза. Как уже говорилось, PPR это режим сборки страницы, при котором большинство элементов собираются на этапе сборки и кэшируются, а отдельные элементы собираются по каждому запросу. При этом предсобранная часть сразу отправляется на клиент, а остальные догружаются динамически.

ppr.png
Иллюстрация работы Частичного Пререндеринга. Источник: next.js docs

Сам по себе функционал был представлен ещё полгода назад в релиз кандидате в качестве экспериментального 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

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

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 (”в меру его большей стабильности и удобства”):

chatgpt-remix.png
Использование Remix в ChatGPT

И начали они, по-видимому, ещё до значительных изменений в next.js:

chatgpt-remix-old.png

В общем, на следующих stateofjs и stackoverflow survey мы, скорее всего, увидим значительные перестановки.

Сама же конференция состоится уже послезавтра, 24 октября, в 19:00 по Москве, в прямой трансляции на сайте nextjs.org/conf и на YouTube. Доклады обещают быть интересными. Также будет интересно послушать про изменения от самой команды next.js (с примерами, анимациями и планами).

Кредиты

Примеры кода или их основы взяты из документации next.js, а также из коммитов, PR и ядра next.js;

Постскриптум

Если вам нужен инструмент генерации документации на основе MD файлов - посмотрите на robindoc.com, если вы работаете с next.js - возможно вы найдёте полезное в решениях nimpl.tech.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Становится ли next.js лучше с этим обновлением?
16.67% Да, теперь всё отлично2
33.33% Да, но всё ещё недостаточно4
0% Осталось без изменений0
8.33% Нет, стало ещё хуже1
8.33% Нет, нужно использовать Remix1
33.33% Посмотреть ответы4
Проголосовали 12 пользователей. Воздержались 2 пользователя.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Стоит ли добавлять в статьи ссылки на коммиты и PR?
62.5% Да5
25% Нет2
12.5% Посмотреть ответы1
Проголосовали 8 пользователей. Воздержались 2 пользователя.