Обычное React-приложение живёт в браузере: сервер отдаёт почти пустой HTML со ссылкой на скрипт, скрипт скачивается, запускается и рисует страницу. Для админки за паролем это нормально — там некому смотреть на пустую страницу первые секунды, и поисковикам туда не нужно. Для публичного сайта это два ощутимых минуса сразу: пользователь видит пустоту, пока грузится скрипт, а поисковый робот видит страницу без содержимого.
Next.js решает обе задачи тем, что часть работы переносит на сервер. Разберём, что именно там происходит и когда это оправдано.
Что не так с рендером только в браузере
Посмотрим, что приходит из сети у обычного приложения на Vite:
<body>
<div id="root"></div>
<script type="module" src="/assets/index-a1b2c3.js"></script>
</body>
Всё содержимое страницы появится только после того, как браузер скачает и выполнит скрипт, а скрипт сходит за данными. Порядок событий такой:
- HTML пришёл — экран пустой.
- Скачался скрипт — React смонтировался, показал заглушку загрузки.
- Пришли данные — появилось содержимое.
На быстром ноутбуке это доли секунды, на телефоне в метро — несколько секунд пустого экрана. И всё это время в исходном коде страницы нет ни заголовка, ни текста: поисковик может выполнить скрипт, но делает это не всегда и не сразу.
Серверный рендер: HTML приходит готовым
При серверном рендере (server-side rendering, SSR) те же React-компоненты выполняются на сервере, и в браузер приходит готовая разметка:
<body>
<div id="root">
<h1>Беспроводная мышь</h1>
<p class="price">1990 ₽</p>
</div>
<script type="module" src="/_next/static/chunks/main-a1b2c3.js"></script>
</body>
Пользователь видит содержимое сразу, поисковик тоже. Дальше скрипт всё-таки скачивается и «оживляет» разметку — навешивает обработчики событий, подключает состояние. Этот шаг называется гидратацией: React проходит по готовому HTML и связывает его со своим деревом компонентов, не перерисовывая заново.
Отсюда правило, о которое спотыкаются все: то, что отрисовал сервер, и то, что React ожидает увидеть на клиенте, должно совпадать. Если совпадения нет, в консоли появится ошибка гидратации. Классический источник — данные, которые на сервере и на клиенте разные:
// Так делать нельзя: на сервере одно время, на клиенте другое
export function Clock() {
return <span>{new Date().toLocaleTimeString()}</span>;
}
Такие вещи выносят в эффект, который выполняется только в браузере, или откладывают до гидратации.
Серверные компоненты
Серверный рендер решил задачу первого экрана, но не убрал главную тяжесть: весь код компонентов всё равно уезжает в браузер. Разметка таблицы, библиотека форматирования дат, разбор Markdown — всё это скачивает пользователь, даже если оно нужно ровно один раз при отрисовке.
Серверные компоненты (React Server Components, RSC) — попытка это исправить. Компонент выполняется на сервере, в браузер уезжает только его результат, а сам код — нет. В Next.js на роутере приложений (app/) компоненты серверные по умолчанию:
// app/products/[id]/page.tsx — серверный компонент
import { db } from "@/shared/db";
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findById(params.id); // прямо в компоненте
return (
<article>
<h1>{product.title}</h1>
<p>{product.price} ₽</p>
</article>
);
}
Обрати внимание на две вещи. Компонент помечен async и ждёт данные прямо внутри — никакого useEffect и состояния загрузки. И он обращается к базе напрямую: этот код в браузер не попадёт, значит ни строки подключения, ни SQL там не окажется.
Цена — серверный компонент не умеет того, что требует браузера: ни состояния, ни эффектов, ни обработчиков событий. Как только нужен клик, поле ввода или useState, компонент становится клиентским. Это объявляется директивой в первой строке файла:
"use client";
import { useState } from "react";
export function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(1);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>+</button>
<span>{count}</span>
</div>
);
}
Практический приём: держать клиентскими листья дерева, а не ветки. Страница и разметка — серверные, а интерактивный кусок — маленький клиентский компонент внутри. Если поставить "use client" на страницу целиком, всё её поддерево станет клиентским, и смысл затеи потеряется.
Server Actions: форма без ручного запроса
Обычный путь отправки формы такой: собрать данные, вызвать fetch, обработать ответ, обновить состояние, не забыть про ошибки. Для этого приходится завести ручку на бэкенде и клиентский код к ней.
Server Actions позволяют объявить серверную функцию и вызвать её из формы напрямую:
// app/products/new/page.tsx
import { db } from "@/shared/db";
import { revalidatePath } from "next/cache";
async function createProduct(formData: FormData) {
"use server";
const title = String(formData.get("title"));
await db.product.create({ title });
revalidatePath("/products");
}
export default function NewProductPage() {
return (
<form action={createProduct}>
<input name="title" required />
<button type="submit">Создать</button>
</form>
);
}
Директива "use server" говорит: эта функция выполняется на сервере, а Next сам сделает под неё запрос. Форма работает даже до того, как загрузился скрипт, — потому что это обычная HTML-форма с обработчиком на сервере.
Тут есть ловушка, о которой стоит знать сразу: серверное действие — это публичная точка входа, такая же, как обычная ручка API. Вызвать её может кто угодно, подделав запрос. Проверять права и валидировать данные внутри действия нужно ровно так же, как в обычном контроллере:
async function deleteProduct(id: string) {
"use server";
const user = await currentUser();
if (!user || !user.canManage(id)) {
throw new Error("Нельзя удалить чужой товар");
}
await db.product.delete(id);
}
Когда Next.js нужен, а когда нет
Признак, по которому выбор делается почти механически:
- Публичный сайт, который должен находиться в поиске и быстро показывать первый экран — Next.js. Витрина товаров, блог, документация, лендинг.
- Приложение за авторизацией — обычное приложение на Vite. Админка, дашборд, личный кабинет: поисковику туда нельзя, а пустой экран на секунду видит только сотрудник, который и так каждый день сюда заходит.
Признак неверного выбора: в приложении за авторизацией понадобилась индексация — значит, выбрали не то. И наоборот: если на публичном сайте не нужен ни поиск, ни первый экран, серверный рендер платит зря — за него приходится держать работающий сервер, а не просто раздачу файлов.
Отдельный случай — контент, который меняется редко: статьи, документация, лендинги. Для него серверный рендер при каждом запросе не нужен, страницы можно собрать заранее. Об этом — в статье про статические сайты.
Что меняется в структуре проекта
Маршруты в Next задаются файлами: путь в файловой системе и есть адрес страницы.
app/
├── layout.tsx общая обёртка всех страниц
├── page.tsx /
├── products/
│ ├── page.tsx /products
│ └── [id]/
│ └── page.tsx /products/42
└── api/
└── health/route.ts /api/health
Специальные файлы рядом со страницей закрывают состояния, которые в обычном приложении пишут руками: loading.tsx показывается, пока страница ждёт данные, error.tsx — если она упала, not-found.tsx — если данных нет.
При этом сама архитектура кода от Next не зависит: слои и границы остаются теми же, что и в приложении на Vite. Разумный приём — держать app/ тонким: страница только собирает нужные куски, а логика лежит в слоях, как описано в статье про Feature-Sliced Design.
Коротко
- В обычном React-приложении HTML пустой: содержимое появляется после загрузки скрипта. Для публичного сайта это плохо и для пользователя, и для поиска.
- Серверный рендер выполняет компоненты на сервере и отдаёт готовую разметку; затем происходит гидратация — скрипт «оживляет» готовый HTML.
- Разметка на сервере и на клиенте должна совпадать, иначе будет ошибка гидратации: время, случайные значения и данные из браузера — частая причина.
- Серверные компоненты выполняются только на сервере, их код не уезжает в браузер и может ходить в базу напрямую; состояния и обработчиков в них нет.
- Клиентскими делают листья дерева, а не страницы целиком:
"use client"распространяется на всё поддерево. - Server Actions позволяют вызвать серверную функцию из формы без ручного запроса, но это публичная точка входа: права и валидация обязательны.
- Next нужен публичным сайтам ради поиска и первого экрана; за авторизацией он чаще лишний.
Что почитать дальше
- Статические сайты: сборка заранее и MDX — когда сервер при каждом запросе не нужен вовсе.
- Feature-Sliced Design — как разложить код по слоям независимо от фреймворка.
- Роутинг в React — маршруты в приложении, которое живёт в браузере.