Разработка · 7 мин чтения

App Router Next.js: как устроен и чем отличается от Pages Router

App Router Next.js - модель маршрутизации, которая стала стабильной в Next.js 13.4 (май 2023), а начиная с 14‑й и 15‑й версии идёт как способ по умолчанию при create-next-app. Я перевёл на неё несколько боевых проектов - от лендингов до админ-панелей на React - и здесь разбираю по слоям, как устроена папка app, чем она отличается от привычного pages, где миграция реально окупается, а где добавит работы без выгоды. Без абстрактной теории: смотрим на структуру файлов, серверные компоненты и загрузку данных так, как это выглядит в рабочем репозитории.

Что делает App Router и почему Vercel сменил маршрутизацию

Оба роутера решают одну задачу - превратить структуру папок в маршруты сайта. Разница в модели рендеринга. В Pages Router каждый файл в pages/ становится страницей, и почти весь код по умолчанию едет в браузер как клиентский React. В App Router компоненты по умолчанию серверные - React Server Components (RSC). Они выполняются на сервере, отдают готовую разметку, а в бандл клиента попадает только то, что помечено директивой 'use client'.

Эффект видно на цифрах. На одном лендинге после переноса каталога товаров на серверные компоненты first-load JS упал со 180 КБ до примерно 90 КБ - почти вдвое, потому что логика фильтрации и запросы к API остались на сервере и в браузер уже не уезжали. Для страницы, где важна скорость первого экрана и SEO, это ощутимая разница по метрикам Core Web Vitals.

Вторая причина смены - вложенные шаблоны и стриминг. В Pages Router общий layout приходилось городить через _app.js и ручные обёртки. App Router даёт вложенные layout.js, частичную загрузку через Suspense и отдельные состояния загрузки и ошибок на уровне сегмента. Это не косметика, а другой подход к архитектуре страницы.

Структура папок: app против pages

В Pages Router маршрут - это просто файл. pages/about.js становится /about, pages/blog/[slug].js - динамической страницей. Коротко и понятно, но вся служебная логика (layout, обработка ошибок, загрузка) живёт отдельно от маршрута.

В App Router маршрут - это папка, а роль файла определяется его именем. Сама папка задаёт сегмент URL, а маршрут появляется только когда внутри есть page.js.

app/
  layout.js        // общий каркас для всех страниц
  page.js          // маршрут /
  loading.js       // скелетон, пока грузится /
  blog/
    layout.js      // общий layout для раздела блога
    page.js        // маршрут /blog
    [slug]/
      page.js      // маршрут /blog/:slug
      not-found.js // 404 внутри статьи
  cart/
    page.js        // маршрут /cart
    error.js       // экран ошибки для корзины

Ключевые служебные файлы, которых в Pages Router просто не было:

  • page.js - делает сегмент публичным маршрутом. Нет файла - нет URL.
  • layout.js - обёртка, которая переиспользуется для всех вложенных страниц и не перемонтируется при навигации между ними.
  • loading.js - автоматически показывается через Suspense, пока грузятся данные сегмента.
  • error.js - ловит ошибку рендера внутри сегмента, обязательно клиентский компонент.
  • route.js - заменяет pages/api/*, здесь живут обработчики API-запросов.

Отдельный приятный момент - папки в круглых скобках, например (marketing). Они группируют маршруты, но не влияют на URL: удобно, когда у промо-страниц и кабинета нужны разные layout, а адреса остаются плоскими.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Серверные и клиентские компоненты: React Server Components на практике

Это главная перемена, из-за которой Pages Router нельзя один в один переложить в app. По умолчанию каждый компонент серверный. Он не попадает в бандл, не имеет доступа к useState, useEffect и браузерным API, зато спокойно ходит в базу, читает переменные окружения и обращается к платным API без риска утечь ключом в клиент.

Как только компоненту нужна интерактивность - состояние, обработчики кликов, хуки - я ставлю 'use client' в первой строке файла. С этого момента компонент и всё, что он импортирует, становится клиентским.

// app/product/[id]/page.js — серверный компонент
async function getProduct(id) {
  const res = await fetch(`https://api.shop.ru/product/${id}`, {
    next: { revalidate: 60 }, // кэш на 60 секунд
  });
  return res.json();
}

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return (
    <article>
      <h1>{product.title}</h1>
      <AddToCartButton id={product.id} /> {/* это уже клиентский */}
    </article>
  );
}

Практическое правило, к которому я пришёл: держите клиентскую границу как можно ниже по дереву. Не вешайте 'use client' на всю страницу ради одной кнопки - выносите интерактивный кусок в отдельный маленький компонент, а обёртку оставляйте серверной. На реальной корзине интернет-магазина это дало разницу примерно в 40 КБ клиентского JS только за счёт того, что вычисление итоговой суммы и промокодов осталось на сервере.

Загрузка данных: fetch вместо getServerSideProps

В Pages Router данные тянулись через три экспортируемые функции: getServerSideProps для запроса на каждый заход, getStaticProps для сборки и getStaticPaths для динамических путей. Всё это жило рядом с компонентом, но отдельно от него.

В App Router данные грузятся прямо внутри асинхронного серверного компонента через обычный fetch, а поведение кэша задаётся опциями запроса. Это заметно короче и убирает целый класс шаблонного кода.

Задача Pages Router App Router
Данные при каждом запросе getServerSideProps fetch(url, { cache: ‘no-store’ })
Статика на сборке getStaticProps fetch(url) - кэш по умолчанию
Обновление по интервалу revalidate в getStaticProps fetch(url, { next: { revalidate: 60 } })
Динамические пути getStaticPaths generateStaticParams()

Серверный fetch особенно выручает на интеграциях, где нельзя светить ключи. Когда я подключал эквайринг T‑Bank и расчёт доставки СДЭК на витрине, все запросы с секретными токенами ушли в серверные компоненты и route.js-обработчики - в браузер не попадает ни строчки секретов, а на клиенте остаётся только форма и кнопка оплаты. Если нужен лендинг на Next.js с серверной интеграцией платежей и доставки, именно такая раскладка данных экономит и трафик, и нервы на аудите безопасности.

Вложенные layout, loading и error: чего не хватало в Pages Router

Вложенность - вторая по важности причина, ради которой я перехожу на App Router на новых проектах. Каждый сегмент может иметь свой layout.js, и эти layout вкладываются друг в друга. Шапка сайта, боковое меню кабинета, футер - всё описывается один раз на нужном уровне и не перерисовывается при переходах внутри раздела. В Pages Router такое приходилось эмулировать через паттерн getLayout на странице.

Файл loading.js автоматически оборачивает сегмент в Suspense. Пока грузятся данные, пользователь видит скелетон, а не белый экран, - и это работает без единой строчки ручного кода со спиннерами. На дашборде с тяжёлыми графиками стриминг дал ощущение, что интерфейс появляется мгновенно: каркас и меню рендерятся сразу, а виджеты с данными подтягиваются по мере готовности.

// app/dashboard/loading.js
export default function Loading() {
  return <div className="skeleton">Загружаем данные…</div>;
}

// app/dashboard/error.js — обязательно клиентский
'use client';
export default function Error({ error, reset }) {
  return (
    <div>
      <p>Не удалось загрузить: {error.message}</p>
      <button onClick={reset}>Повторить</button>
    </div>
  );
}

Таблица отличий и когда стоит мигрировать

Сведу разницу в одну таблицу, чтобы было видно целиком.

Критерий Pages Router App Router
Папка pages/ app/
Маршрут файл папка + page.js
Компоненты по умолчанию клиентские серверные (RSC)
Данные getServerSideProps / getStaticProps async-компонент + fetch
Общий каркас _app.js, getLayout вложенные layout.js
Состояние загрузки вручную loading.js + Suspense
API-роуты pages/api/* route.js
Статус поддерживается рекомендуется по умолчанию

Важный момент: оба роутера работают в одном проекте одновременно. Vercel не заставляет переписывать всё разом - app и pages уживаются, и мигрировать можно по одному разделу. Я обычно начинаю с новых страниц: их сразу собираю в App Router, а старые переношу постепенно, по мере того как их всё равно приходится трогать.

Где миграция окупается: новый проект с прицелом на SEO и скорость, много статики или контента с редким обновлением, сложная вложенная навигация с кабинетами и разделами. Где я бы не спешил: стабильный работающий проект на Pages Router без жалоб на производительность, тяжёлая завязка на клиентские библиотеки, которые ещё не дружат с RSC, или сжатые сроки, когда переписывать data fetching просто некогда. Переход - это не косметика, а смена модели загрузки данных, и закладывать на него стоит реальные часы, а не полдня.

Если команда впервые берёт App Router, первый месяц уходит на привыкание к границе сервер-клиент - самая частая ошибка новичков это useState в серверном компоненте и упавшая сборка. Дальше становится комфортно, и назад на Pages Router уже не хочется.

Быстрый SEO-лендинг под продукт

Лендинг на Next.js

от 80 000 ₽

Подробнее →

Частые вопросы

App Router полностью заменил Pages Router?

Нет. Pages Router поддерживается и остаётся рабочим вариантом, его не объявляли устаревшим. App Router - рекомендуемый способ для новых проектов, но оба роутера можно держать в одном приложении и переносить страницы постепенно, без большого переписывания за раз.

Обязательно ли переписывать проект на App Router?

Если текущий проект на Pages Router работает и по скорости всё устраивает, срочной необходимости нет. Смысл появляется, когда нужны серверные компоненты ради облегчения бандла, вложенные layout или стриминг с частичной загрузкой. На новом проекте я почти всегда стартую сразу с App Router.

Чем серверные компоненты отличаются от getServerSideProps?

getServerSideProps выполнялся на сервере, но результат прокидывался в клиентский компонент, который всё равно попадал в бандл. Серверный компонент сам выполняется на сервере и в браузер не отправляется вовсе - в клиент едет только готовая разметка и явно помеченные 'use client' куски. Отсюда меньше JS и невозможность случайно раскрыть секретный ключ.

Сколько времени занимает миграция с Pages Router на App Router?

Зависит от размера. Небольшой лендинг на 5-7 страниц я переношу за 2-3 дня вместе с переделкой загрузки данных. Крупный магазин или админку с десятками маршрутов и интеграциями стоит планировать на несколько недель и переводить по разделам, а не одним заходом, чтобы не заморозить разработку остального.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно