Next.js или React: карта URL, данных и рендера помогает выбрать основу приложения. Next.js — это React-фреймворк, а React — библиотека, на которой собирают стек сами. Карта URL, данных и рендера помогает выбрать режим для каждого маршрута: закрытый кабинет живёт на CSR, каталог с ценой — на динамическом серверном запросе, публичные страницы — на static export. Ниже разобраны три приложения A, B и C с расчётами свежести и раздельными конфигами.
Next.js как React-фреймворк и три сценария
В документации React сказано: для нового приложения или сайта команда рекомендует начинать с фреймворка, а сборку с нуля оставляет для случаев, когда готовые фреймворки плохо подходят под ограничения проекта. Там же перечислены рекомендованные full-stack фреймворки, и Next.js с App Router назван React-фреймворком, который использует архитектуру React для full-stack приложений. React Router и Expo идут отдельными строками: первый — популярная библиотека маршрутизации, второй — фреймворк для нативных приложений. Вывод для выбора: Next.js и React не два взаимоисключающих варианта, а библиотека и надстройка над ней.
В обзоре на lytvynov-production.com разобраны три сценария. Первый — дашборд или личный кабинет: закрытые данные, авторизация на API, клиентская отрисовка. Второй — каталог с ценой: данные меняются, и показанная цена обязана быть свежей. Третий — публичные страницы: контент обновляется редко, важны начальный HTML и статический хостинг. Эти сценарии совпадают с картой A, B и C ниже, и их можно применить к своему проекту без переписывания всего приложения.
Документация React отдельно отмечает: full-stack фреймворки поддерживают клиентскую отрисовку, одностраничные приложения и статическую генерацию, а серверные возможности подключаются на уровне отдельного маршрута. Это ключ к карте: один проект может держать разные режимы для разных URL. Начинать можно с клиентского приложения, а серверные функции добавлять позже на конкретных маршрутах.
| Сценарий | Данные | Режим | Runtime |
|---|---|---|---|
| A, /app/calc | Закрытый калькулятор, /api/v1/calc | Существующий React/Vite CSR | 0 новых Node-runtime |
| B, /catalog/sku-17 | Цена с updated_at, давность ≤60 с | Динамический запрос на запрос | 1 server-runtime |
| C, / и /blog/start | Публичный контент, давность 24 ч | Static export/SSG | 0 новых Node-runtime |
Карта URL, данных и давности
Карта строится по трём вопросам к каждому маршруту: какие данные он показывает, как быстро они устаревают и какой runtime нужен после публикации. Для A, /app/calc публичного контента нет: калькулятор работает с существующим /api/v1/calc, а проверка доступа живёт на API. Клиентская отрисовка сама по себе авторизацию не обеспечивает и обход API не запрещает, поэтому защита остаётся задачей серверной части. Новых Node-runtime для A не появляется, bundle остаётся статическим.
Для B, /catalog/sku-17 данные приходят вместе с updated_at. Название «Лампа Л17», цена меняется в 12:00:00 с 1200 на 1250 ₽, а допустимая давность показанной цены — не больше 60 секунд. Источник обязан возвращать актуальный timestamp, иначе проверить возраст невозможно. Для C, / и /blog/start данные публичные: H1 «Свет для мастерской» и заголовок статьи «Как подготовить прибор». Обновление идёт не чаще раза в день, допустимая давность — 24 часа, cookies, server actions и SSR-зависимости на этих маршрутах не нужны.
Давность — это договор с читателем. Если цена старше допустимого возраста, интерфейс показывает «Уточнить цену» вместо молча устаревшего числа. Если источник недоступен, действует то же правило. Такой договор фиксируется до выбора технологии и определяет, где появится серверный runtime, а где хватит статических файлов.
| URL | Данные | Давность | Что показываем при просрочке |
|---|---|---|---|
| /app/calc | /api/v1/calc | по правилам API | состояние авторизации |
| /catalog/sku-17 | цена + updated_at | ≤60 с | «Уточнить цену» |
| /blog/start | статья | 24 ч | предыдущую версию |
Три расчёта свежести цены
Свежесть считается как разница между временем ответа сервера и updated_at источника. Ниже три учебных строки в одном часовом поясе с согласованными часами; это ручной расчёт политики.
| Время ответа | updated_at | Возраст | Вид цены |
|---|---|---|---|
| 12:00:30 | 12:00:00, price=1250 | 30 с | 1250 ₽ |
| 12:01:10 | 12:00:00, price=1250 | 70 с | «Уточнить цену» |
| 12:01:10 | 12:01:00, price=1250 | 10 с | 1250 ₽ |
Первая строка укладывается в 60 секунд, цена показывается. Вторая строка даёт возраст 70 секунд, и вместо старого числа выводится «Уточнить цену». Третья строка показывает, что при свежем timestamp цена снова отображается, даже если предыдущий ответ был просрочен. Слово «Уточнить» здесь — ожидаемый результат политики.
Порядок проверки для своего проекта: возьмите время ответа сервера, вычтите updated_at, сравните с допустимой давностью, выберите показ или заглушку. Добавьте обработку недоступного источника: она ведёт к тому же «Уточнить цену». Соблюдение такого SLO требует проверки источника и кешей, потому что возраст данных зависит от них.
Частая ошибка — считать, что частота обновлений сама выбирает режим. Формулировка «цена меняется несколько раз в день» не говорит, нужен ли ISR или динамический запрос. Режим выбирает допустимая давность и требование к строгости: для условия «не старше 60 секунд» в карте выбран динамический запрос с отдельной проверкой timestamp.
Почему revalidate=60 не даёт строгих 60 секунд
В документации Next.js по ISR описан порядок: страница пререндерится, запросы обслуживаются из кеша, после истечения интервала следующий запрос всё ещё получает устаревшую страницу, а новая версия генерируется в фоне. Когда генерация завершается, следующий запрос получает обновлённую страницу. Там же для точной свежести рекомендуют on-demand ревалидацию, а для данных в реальном времени — переход на динамический рендеринг.
Из этого следует разделение. ISR подходит, когда допустимо отдать устаревшую страницу на время фоновой регенерации. Динамический запрос подходит, когда каждый ответ обязан проверять возраст данных. Проверка timestamp — отдельный шаг, который сравнивает updated_at с текущим временем и решает, показывать цену или заглушку. Один revalidate=60 не заменяет эту проверку: он задаёт интервал ревалидации кеша.
Документация перечисляет и ограничения: ISR работает на Node.js runtime и не поддерживается при static export. Если на маршруте несколько запросов с разными интервалами, для ISR берётся наименьший, а запрос с revalidate 0 или no-store переводит маршрут в динамический рендеринг. При нескольких инстансах файловый кеш остаётся на инстанс, и для согласованной ревалидации нужен общий cache handler.
Практический вывод для B: динамический запрос на каждый показ цены, fetch без кеша, проверка возраста по updated_at. ISR остаётся вариантом для страниц, где устаревшая версия на время регенерации допустима по договору давности.
Static export для публичной части
Для C в документации Next.js по static export указано: при сборке генерируется HTML-файл на каждый маршрут, а результат кладётся в папку out. Такой экспорт размещается на любом веб-сервере, который отдаёт HTML, CSS и JS. Конфиг проекта C задаётся полностью:
export default { output: 'export', trailingSlash: true };
После next build появляется out/ с обоими публичными маршрутами. Статический хост направляет пути на нужные файлы; серверные rewrites Next.js здесь не используются. Сборка может требовать Node на CI, однако после публикации постоянный Node-runtime для публичных страниц не нужен. Кабинет /app остаётся существующим Vite CSR, API /api/v1/* остаётся прежним.
Документация перечисляет функции, которые со static export не работают: динамические маршруты с dynamicParams: true, cookies, rewrites, redirects, headers, ISR, server actions, draft mode. Server Components при экспорте выполняются во время сборки, а клиентские компоненты пререндерятся в HTML. Поэтому маршруты C заранее освобождают от cookies, server actions и SSR-зависимостей.
Проверка результата: до и после смены стека открывают один и тот же URL /blog/start с одинаковыми данными и без выполнения JS. Документ должен содержать текст «Как подготовить прибор». Это приёмка начального HTML; индексирование проверяют отдельно инструментами поисковика. Личный кабинет и его API при этом не меняются.
Отдельные конфиги B и C и обязанности
Конфиги B и C различаются принципиально. У C включён output: ‘export’, и сборка даёт статические файлы. У B этого режима нет: динамический серверный запрос и static export не смешиваются одним обещанием. Попытка включить export на маршруте с динамической ценой упрётся в список неподдерживаемых функций.
Эксплуатационные обязанности удобно вести перечнем. A — один существующий static build и прежний API. B — дополнительный server-runtime, наблюдение за источником цены и кешами, бюджет на это. C — два статических build-процесса и прежний API, без нового Node-runtime для публичной части. Денежную смету составляют на своей инфраструктуре: цена эксплуатации не выводится из названия фреймворка.
| Проект | Build | Runtime после публикации | Наблюдение |
|---|---|---|---|
| A | существующий static build | прежний API | доступ на API |
| B | сборка с серверным маршрутом | 1 server-runtime | источник цены и кеши |
| C | два статических build | 0 новых Node-runtime | статический хостинг |
Простая смена React на Next не считается доказательством роста SEO или скорости. Выбор делается по карте: какие URL публичные, какая давность данных допустима, какой runtime готовы поддерживать. Такой порядок даёт проверяемое решение вместо спора о фреймворках.
Частые вопросы
Next.js — это React или отдельная библиотека?
Next.js — React-фреймворк: в документации React он назван фреймворком с App Router, который использует архитектуру React для full-stack приложений. React остаётся библиотекой, на которой собирают интерфейс. Выбор идёт между самостоятельно собранным стеком на React и готовым фреймворком.
Когда хватит React с Vite, а когда нужен Next.js?
В сценарии A сохранён React с Vite: закрытый калькулятор получает данные через существующий API, bundle статический. Для B в карте выбран Next.js с динамическим запросом и проверкой timestamp, а для публичного C — отдельный static export. Это решения для заданных условий карты.
Можно ли обойтись revalidate=60 для свежей цены?
Один revalidate=60 задаёт интервал ревалидации кеша. В документации Next.js по ISR сказано, что после интервала следующий запрос может получить устаревшую страницу, пока новая генерируется в фоне. Для строгого условия «не старше 60 секунд» в карте выбран динамический запрос и проверка timestamp.
Что проверять, чтобы убедиться в статическом HTML?
Откройте один и тот же URL, например /blog/start, до и после смены стека, с одинаковыми данными и без выполнения JS. Документ должен содержать ожидаемый текст статьи. Это приёмка начального HTML; индексирование проверяют отдельно инструментами поисковика.
Что меняется в эксплуатации при переходе на Next.js?
В сценарии B добавляются server-runtime, наблюдение за источником данных и кешами и бюджет на них. В сценарии C остаются два статических build-процесса и прежний API, без нового Node-runtime для публичной части; Node может потребоваться при сборке на CI. Денежную смету считают на своей инфраструктуре.
