Проблемы Next.js — конструктивная критика одной из самых популярных платформ разработки сайтов

Проблемы развертывания Next.js или почему платформам необходимо улучшать взаимодействие с открытым исходным кодом (OpenSource)?

Познакомимся с Next.js еще разок

Next.js — это фреймворк для веб-разработки с открытым исходным кодом, созданный и управляемый компанией Vercel, облачным провайдером, предоставляющим услуги хостинга Next.js.

Инновационные решения фреймворка поверх React сделали его предпочтительным инструментом для многих разработчиков, и не без причины: он представил такие концепции, как встроенная поддержка SSR, SSG, ISR и API-маршрутов.

В Netlify мы гордимся тем, что предоставляем полноценную поддержку Next.js, обеспечивая разработчикам выбор вариантов развертывания без потери функциональности. Однако поддержка такого уровня для Next.js требует значительных ресурсов и создает уникальные инженерные задачи. Эти проблемы касаются не только Netlify, но и других платформ, таких как Cloudflare, AWS Amplify Hosting, SST, Google Firebase App Hosting и Microsoft Azure Static Web Apps.

Давайте заглянем за кулисы работы инженеров Netlify по поддержанию паритета функциональности с платформой Vercel и отслеживанию постоянного потока изменений в Next.js.

Примечание о текущем моменте

Мы некоторое время придерживали эту публикацию, пытаясь найти наилучший способ представить изложенные ниже факты и предложения так, чтобы это не выглядело как критический разбор. Всё-таки мы конкурируем с Vercel на рынке платформ.

Затем произошел недавний инцидент с безопасностью в промежуточном ПО Next.js, который вызвал небольшой переполох в соцсетях и критику относительно того, как была решена эта проблема.

Эта статья не об этом инциденте безопасности. Она о более широком контексте текущих проблем в обеспечении корректной работы Next.js для клиентов. Многие из этих проблем — как и, пожалуй, реакция на инцидент — связаны с закрытым характером поддержки Next.js. Однако сейчас появилось искреннее стремление всех участников решить эту проблему.

Наша цель здесь — дать более полное представление о существующих трудностях и наметить путь к дальнейшему сотрудничеству.

Проблема №1: отсутствие поддержки адаптеров

Главное преимущество программного обеспечения с открытым исходным кодом — его переносимость. Разработчики и организации должны иметь возможность свободно переходить между различными провайдерами без страха оказаться в зависимости от конкретного поставщика. Например, CLI утилита npm работает как с реестром npm, Inc., так и со всеми сторонними реестрами, как и Docker с DockerHub, и множество других примеров.

Для достижения этой цели большинство современных веб-фреймворков используют концепцию адаптеров, плагинов или пресетов, чтобы адаптировать выходные данные фреймворка под конкретную целевую платформу развертывания. Провайдерам это необходимо для подготовки и настройки инфраструктуры, требуемой для работы ваших приложений. Примерами являются Remix, Astro, SvelteKit, Gatsby и Qwik.

Фреймворк (Astro) с полноценной поддержкой взаимозаменяемых адаптеров сборки, каждый из которых предназначен для определенной среды развертывания или провайдера

Некоторые фреймворки идут еще дальше: Nuxt, Analog, SolidStart и TanStack Start используют один и тот же базовый механизм (Nitro) и, таким образом, совместно используют пресеты целевых платформ развертывания. Желаемая полезность и масштаб таких общих основ — это быстро развивающаяся тема с различными мнениями, которые открыто обсуждаются, как и должно быть.

Фреймворки на базе Nitro также имеют полноценную поддержку взаимозаменяемых адаптеров сборки, но идут дальше, используя общие адаптеры

Такие паттерны позволяют фронтенд-разработчикам сохранять основной код нетронутым и просто менять адаптер, если они решат развернуть приложение у другого провайдера.

Эти адаптеры могут поддерживаться авторами фреймворка, хостинг-провайдерами, сообществом или всеми вместе. Фреймворки обычно структурированы так, что любой может создать свой собственный адаптер, если нет готового для нужного провайдера. Это возможно благодаря наличию публично документированной спецификации адаптера. Это также означает меньше сюрпризов, так как изменения в интерфейсе адаптера следуют принципам семантического версионирования.

Со своей стороны, платформенные вендоры обычно имеют документированный API для взаимодействия фреймворков с платформой, который может использовать любой фреймворк. В нашем случае это Netlify Frameworks API.

Как Netlify собирает сайт, используя фреймворк (Astro) с адаптером сборки Netlify, в качестве примера

Уникальная сложность с Next.js заключается в том, что, хотя у Vercel (платформы) есть Build Output API для фреймворков с 2022 года, сам Next.js не соответствует этому API и не имеет механизма адаптеров, через который другие участники могли бы обеспечить поддержку другой платформы. Вместо этого сборки Next.js используют частный, в значительной степени недокументированный формат, который может меняться.

Вследствие этого провайдерам, таким как Netlify, Cloudflare, AWS Amplify Hosting, SST, Google Firebase App Hosting и Microsoft Azure Static Web Apps, приходится считывать ориентированный на Vercel, частично недокументированный результат сборки с диска, преобразовывать его в свой формат и записывать обратно на диск.

Сборки Next.js совместимы только с Vercel, поэтому другим платформам (таким как Netlify) приходится постфактум преобразовывать их в свой формат

В Netlify мы автоматически запускаем плагин сборки (адаптер OpenNext для Netlify) после сборки Next.js. В сочетании с комплексным автоматизированным тестированием (как собственными тестами фреймворка, которые мы запускаем, так и нашими тестовыми наборами), конечным результатом является надёжная работа Next.js на Netlify. Для большинства сайтов это работает сразу, без дополнительной настройки. Однако реализация была бы намного проще, легче в обслуживании и доступнее для участия сообщества, если бы Next.js следовал установленному паттерну адаптеров. Это обсуждалось время от времени, но по-настоящему набрало обороты после того, как группа OpenNext расширилась, чтобы чётко озвучить потребности множества участников. Теперь это наконец сдвинулось с мёртвой точки!

Проблема №2: Отсутствие документации для продакшн уровня self-hosted

Документация по развертыванию Next.js перечисляет следующие варианты развертывания:

Вы можете развернуть управляемый Next.js с помощью Vercel или выполнить самостоятельное размещение на Node.js сервере, Docker-образе или даже в виде статических HTML-файлов.

Здесь «Node.js сервер» подразумевает один сервер Node.js. Единственный экземпляр Node.js сервера (с Docker или без него) без горизонтального масштабирования и развертывания без простоев не является жизнеспособной стратегией развертывания для серьезных проектов, а полностью статические сайты в наши дни покрывают лишь ограниченные варианты использования Next.js. «Из коробки» это не поддерживает многие функции, которые делают Next.js мощным при масштабировании: пограничные промежуточные обработчики, глобально персистентное кэширование страниц и выборки данных, обеспечивающее инкрементальную статическую регенерацию и ревалидацию по требованию, если назвать некоторые.

Документация по самостоятельному размещению кратко затрагивает некоторые сложности (выделение наше):

Кэширование и ревалидация страниц (с использованием Инкрементальной Статической Регенерации (ISR) или новых функций в App Router) используют один и тот же общий кэш.[…]По умолчанию сгенерированные файлы кэша хранятся в оперативной памяти (по умолчанию 50 МБ) и на диске. Если вы размещаете Next.js с использованием платформы оркестрации контейнеров, такой как Kubernetes, каждый под будет иметь свою копию кэша. Чтобы предотвратить отображение устаревших данных, поскольку кэш по умолчанию не распределяется между подами, вы можете настроить кэш Next.js, предоставив обработчик кэша и отключив кэширование в памяти.

Проблема устаревших данных сложнее, чем кажется. Например, поскольку у каждого узла есть свой собственный кэш, если вы используете revalidatePath в серверном действии или обработчике маршрута, этот код будет выполняться только на одном из узлов, который обрабатывает это действие/маршрут, и очистит кэш только для этого узла.

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

Поскольку Next.js является основным фреймворком с широким распространением, мы полностью поддерживаем его — это означает, что наша инженерная команда, занимающаяся поддержкой фреймворков на нашей платформе, накопила необходимые знания и реализовала собственный обработчик кэша с соответствующими тестами. Если что-то меняется в предстоящей canary-версии, мы узнаем об этом оперативно.

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

Вы тоже должны суметь это реализовать

Даже при максимальном использовании наших существующих возможностей, обработчик кэша Netlify составляет около 500 строк кода и 500 строк тестов — это далеко не тривиальная задача. Такой объем инвестиций недоступен для большинства разработчиков, рассматривающих вариант самостоятельного хостинга.

Поскольку Next.js абстрагирует многие из этих деталей времени выполнения, существует острая необходимость расширить концепцию адаптера для поддержки не только вопросов времени сборки. Например, интерфейс CacheHandler для платформ, вероятно, следует переосмыслить так, чтобы он требовал минимального количества логики фреймворка (которая всегда подвержена изменениям) на стороне платформы.

В настоящее время это выходит за рамки адаптера времени сборки, обсуждаемого между группой OpenNext и Vercel, но мы с оптимизмом смотрим на то, что это усиленное сотрудничество на общей площадке приведет к появлению более качественных, простых и документированных вариантов развертывания для разработчиков Next.js.

Проблема №3: Недокументированное поведение

Фреймворк Next.js содержит множество недокументированных опций, функций и особенностей поведения. Чтобы достичь функционального паритета с Vercel, поставщикам платформ приходится изучать их и учитывать в своей работе.

Например, сайты Next.js, развернутые на Vercel, используют свои уникальные пути выполнения кода Next.js через недокументированный minimalMode. Это отключает многие основные функции фреймворка, которые затем предположительно реализуются заново внутри платформы Vercel (которая, естественно, имеет закрытый исходный код). Хотя Netlify этого не делает, некоторые провайдеры даже решили использовать этот недокументированный режим и методом обратной разработки внедрили аналогичный функционал в свои платформы.

Наш подход: проактивное автоматизированное тестирование

Наш адаптер Next.js содержит обширный набор из сотен тестов, которые мы запускаем для Next.js 13, 14, 15 и последней canary-версии на Linux, macOS и Windows. Большинство из них — интеграционные тесты, которые собирают и запускают тестовый сайт Next.js, а многие являются сквозными тестами, которые развертывают тестовый сайт на Netlify.

Конечно, это лишь базовые требования. Кроме того, для каждой тестируемой версии Next.js мы также клонируем исходный git-репозиторий Next.js и запускаем более 1700 сквозных интеграционных тестов, адаптированных для развертывания на Netlify. Эти тесты очень обширны, и поскольку они проверяют весь реальный опыт через тесты Playwright, работающие в браузере как пользователь, они дают нам очень высокую уверенность при выпуске изменений.

Затем мы ежедневно запускаем этот набор тестов, формируем отчет (доступный публично), который автоматически включает аннотации для известных проблем, отслеживаемых на GitHub, и проактивно уведомляем нашу команду по фреймворкам о любых новых неизвестных проблемах.

Это особенно ценно, потому что тестирование проводится не только со стабильными релизами Next.js, но и с предварительными «canary» версиями. Обеспечивая соответствие тестам последней canary-версии, мы можем постепенно добавлять поддержку новых функций и обрабатывать предстоящие критические изменения (как в документированных, так и в недокументированных интерфейсах) еще до их анонсирования и общедоступного релиза. Это действительно необходимо только из-за отсутствия прозрачности дорожной карты и предсказуемости релизов (см. проблемы №5 и №6 ниже).

Проблема №4: Не основан на открытых веб-стандартах

На момент написания статьи, первые четыре слова на сайте фреймворка Remix гласят «Ориентирован на веб-стандарты»; сайт Astro рекламирует «Отсутствие привязки»; SvelteKit призывает «изучать веб-стандарты, работающие во всех средах».

При использовании Incremental Static Regeneration (ISR), Next.js отправляет заголовки ответа Cache-Control, содержащие недействительные директивы Stale-While-Revalidate (SWR), например:

Cache-Control: s-maxage=600, stale-while-revalidate

тогда как RFC 5861 требует указания значения Time-To-Live, например:

Cache-Control: s-maxage=600, stale-while-revalidate=60

Это может показаться незначительной проблемой, возможно, даже не стоящей упоминания. Но последствия такой детали в том, что недействительная директива молча игнорируется многими CDN. Поставщики должны сначала обнаружить существование проблемы, а затем либо решить поддерживать несоответствующие стандартам заголовки ради Next.js, либо включить код для преобразования этого заголовка в свой связующий код Next.js. И хотя теперь можно включить действительные заголовки по умолчанию (см. GitHub #52251, #61330, #65867 и #65887), эта проблема в итоге была закрыта, оставив нестандартные заголовки по умолчанию. Это лишь один пример.

Кроме того, некоторые функции Next.js (например, ISR) реализованы как специальное решение, глубоко интегрированное с функциональностью платформы Vercel. Во многих из этих случаев эти функции могли бы быть построены на основе открытых веб-стандартов (например, стандартных Web API) для большей переносимости. Эта проблема, среди прочих, побудила Кента К. Доддса написать свою содержательную статью «Почему я не буду использовать Next.js».

Продвижение открытого Web

Миссия Netlify — строить лучший Web вместе.

Мы стремимся воплощать это на практике, участвуя в рабочих группах, таких как WinterCG (теперь WinterTC) и инициативах вроде OpenNext, составляя RFC для фреймворков и сообщества в целом, предоставляя стабильную, документированную платформу для партнеров, реализуя поддержку новых стандартов платформы и API по мере развития веба, и в целом отстаивая открытый веб при каждой возможности.

Но для решения наших насущных конкретных потребностей в Next.js нам потребовалось нечто большее:

Примитивы важнее фреймворков

Кент Бек говорит: «Сначала сделайте изменение простым (это может быть сложно), затем сделайте простое изменение». В некотором смысле это было нашей стратегией в течение последнего года. Вместо того чтобы решать каждую новую функцию Next.js по отдельности, мы стремимся определить базовые примитивы платформы, которые сделали бы функцию легко адаптируемой — не только в Next.js, но в любом фреймворке или даже без него.

Например, вместо того чтобы реализовывать Incremental Static Regeneration (ISR) Next.js глубоко внутри нашей платформы, мы реализовали поддержку стандартной директивы Stale-While-Revalidate и других продвинутых примитивов кэширования полностью через простые HTTP-заголовки ответа. В результате ISR Next.js реализован в нашем адаптере Next.js несколькими строками кода, которые просто устанавливают заголовки, и поскольку все остальные SSR-фреймворки позволяют пользователям устанавливать заголовки, эта функциональность автоматически стала доступна этим фреймворкам — или даже без какого-либо фреймворка.

Проблема №5: Отсутствие прозрачной Дорожной карты

Большинство фреймворков публикуют публичную дорожную карту, чтобы держать разработчиков, провайдеров и создателей интеграций в курсе предстоящих новых функций, устареваний, критических изменений и так далее. Смотрите, например: Astro, Remix, Qwik, Nuxt, Angular, а также минималистичные дорожные карты, такие как у Vite и Svelte

У Next.js нет публичной дорожной карты или какой-либо эквивалентной прозрачности.

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

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

Модель управления Next.js утверждает, что «крупные архитектурные решения и функции начинаются с запроса комментариев (RFC)«. Однако на момент написания только четыре RFC от сообщества были приняты за восемь лет, причем среди них только один участник не из Google. Разработка идей, принятие решений и дорожные карты — всё это скрыто за закрытыми дверями и представляется публике дважды в год на конференциях Vercel Conf и Next.js Conf одновременно с новыми релизами.

В сотрудничестве с сообществом (участниками OpenNext и другими) мы в дальнейшем берем на себя обязательство предлагать и вносить вклад в RFC Next.js для решения некоторых из этих проблем, изложенных выше, таких как вышеупомянутый адаптер выходных данных сборки. Давайте строить вместе!

Проблема №6: Отсутствие предсказуемости релизов

Большинство фреймворков либо регулярно обновляют свою публичную дорожную карту (см. выше), либо публично анонсируют предстоящие изменения по мере их разработки (например, Nuxt), либо придерживаются фиксированного графика релизов (например, Angular).

Ничего из этого не относится к Next.js. Разработчики, хостинг-провайдеры, создатели интеграций и сообщество в целом вынуждены строить догадки и читать между строк.

Например, релиз-кандидат Next.js 15 был анонсирован в мае, но никаких обновлений не было представлено между тем моментом и следующим релиз-кандидатом пять месяцев спустя (за которым через несколько дней последовал стабильный релиз Next.js 15), кроме того, что можно было узнать, изучая 2,254 коммита в ветке canary за этот период.

Как мы отслеживаем релизы и пререлизы Next.js

Мы следим за PR и релизами Next.js как ястребы. Фактически, мы тратили на это столько времени, что создали небольшой сервис для автоматизации рутинной работы.

Будьте уверены, что команды в Cloudflare, AWS Amplify Hosting, SST, Google Firebase App Hosting, Microsoft Azure Static Web Apps и других прибегают к тому же.

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

Смотрим в будущее

Давайте подведем итоги о текущей ситуации и предпринятых конкретных шагах:

Во-первых, мы берем на себя обязательство сотрудничать с другими провайдерами, сообществом и командой Next.js. У нас всех общая цель — обеспечить отличный опыт для разработчиков Next.js и посетителей их сайтов. Именно поэтому мы присоединились к инициативе OpenNext вместе с командами SST и Cloudflare.

Во-вторых, благодаря обращению инженеров Vercel, это уже привело к установлению прямых каналов связи с основной командой Next.js. Мы с оптимизмом смотрим на то, что это поможет нам решить проблемы в их источнике.

В-третьих — дела говорят громче слов — согласно документированной модели управления Next.js, мы начнем составлять RFC в сотрудничестве с другими провайдерами для решения некоторых из этих проблем. Этот процесс уже запущен.

Мы с нетерпением ждём возможности вместе создавать лучший веб.

Оригинал https://www.netlify.com/blog/how-we-run-nextjs/

Фото аватара

Анатолий Юмашев

Разбираюсь в технологиях, пишу про сервисы и интеграции

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *