Где провести границу между сервером и клиентом на странице Next.js App Router
Практическая модель мышления для серверных компонентов React: что выполняется где, как страница блога делятся на серверную и клиентскую части, а также правила импорта, которым следует придерживаться.
Компоненты сервера React часто кажутся сложными даже после прочтения документации, в основном потому что они заставляют отказаться от предположения, которое каждый разработчик React держал все годы: что компоненты работают в браузере. Как только это предположение устраняется, остальное следует довольно естественным образом. В этой статье формируется когнитивная модель на примере одной страницы блога, показывается, какие части должны находиться на сервере, а какие — на клиенте, а также приводятся несколько правил, которые помогают сохранять четкое разделение между ними.
Чтобы узнать больше о самом процессе отрисовки, ознакомьтесь с архитектурой отрисовки без использования бандлов с помощью компонентов сервера.
От «все гидрируется» к «только то, что необходимо»
До появления серверных компонентов каждый компонент React в конечном итоге выполнялся в браузере. Даже при рендеринге с серверной стороны весь JavaScript для всей иерархии компонентов отправлялся на клиент, где происходила его инициализация, чтобы приложение стало интерактивным. Это неэффективно для компонентов, которые лишь загружают данные и передают их в качестве свойств: их код загружается и выполняется, но так и не реагирует на действия пользователя.
Серверный компонент меняет ситуацию, выполняясь исключительно на сервере. Его код никогда не отправляется в браузер и не подвергается инициализации, поэтому он не добавляет JavaScript в пакет клиента. В браузер поступает только готовый результат его обработки, который React сериализует в компактный пакет вместе с HTML.
Самый простой способ различать эти два типа:
- Клиентский компонент: выполняется в браузере (после предварительного рендеринга на сервере), может хранить состояние и реагировать на события.
Почему такое разделение имеет смысл
Возьмём типичную страницу блога. Заголовок и текст берутся из базы данных, выглядят одинаково для всех посетителей и не учитывают количество кликов. Традиционно код, отвечающий за их отображение, а также любые библиотеки форматирования в виде Markdown всё равно отправляются в браузер. С использованием компонентов сервера всё это остаётся на сервере. Размер пакета сокращается, и страницы загружаются быстрее, особенно на медленных устройствах.
Команды, переходящие на App Router, построенный на компонентах сервера, сообщают о лучших показателях Core Web Vitals в некоторых сценариях, в частности по показателю LCP. Считайте это зависящим от контекста: преимущество зависит от того, сколько клиентского JavaScript удаётся убрать с ваших страниц, поэтому измеряйте показатели для своих маршрутов.
Страница блога, разделённая на две части
В проекте с App Router от Next.js файл страницы является серверным компонентом, если не указано иное. Приведённая ниже страница представляет собой функцию async, которая ожидает данные непосредственно из слоя данных, обрабатывает случай их отсутствия, отображает статический контент, а затем добавляет один интерактивный элемент для комментариев. Она написана с учётом API Next.js 14:
// app/posts/[slug]/page.tsx
// This is a Server Component by default — no "use client" needed
import { getPostBySlug } from "@/lib/db"
import { PostContent } from "@/components/PostContent"
import { CommentSection } from "@/components/CommentSection"
type Props = {
params: { slug: string }
}
export default async function PostPage({ params }: Props) {
// Direct DB call — no useEffect, no API route, no loading state
const post = await getPostBySlug(params.slug)
if (!post) {
return <div>Post not found.</div>
}
return (
<article className="max-w-2xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold mb-4">{post.title}</h1>
<PostContent content={post.body} />
{/* This one needs interactivity — so it's a Client Component */}
<CommentSection postId={post.id} />
</article>
)
}
Обратите внимание на отсутствующие элементы: нет useState, нет useEffect, нет обёртки для API-маршрута и нет механизма отслеживания состояния загрузки. Данные ожидаются точно так же, как и в любой другой асинхронной функции, что является основой данной модели.
Два практических замечания. Начиная с Next.js 15, параметр params передаётся в виде Promise, поэтому странице необходимо выполнить await params перед чтением значения slug; проверьте версию, на которой вы работаете. Что касается реального случая отсутствия страницы, вызов функции notFound() из модуля next/navigation возвращает настоящий статус 404, а не обычную страницу с сообщением об ошибке.
Поле для комментариев отличается. Оно сохраняет текст черновика в состоянии и реагирует на набор символов и клики, поэтому должно работать в браузере. Директива "use client" в верхней части файла обозначает его как клиентский компонент:
// components/CommentSection.tsx
"use client" // opts into browser rendering
import { useState } from "react"
type Props = {
postId: string
}
export function CommentSection({ postId }: Props) {
const [comment, setComment] = useState("")
const handleSubmit = async () => {
await fetch("/api/comments", {
method: "POST",
body: JSON.stringify({ postId, comment }),
})
setComment("")
}
return (
<div className="mt-8">
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Leave a comment..."
className="w-full border rounded p-2 text-sm"
/>
<button
onClick={handleSubmit}
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded text-sm"
>
Post Comment
</button>
</div>
)
}
Здесь находится всё интерактивное: локальное состояние для текстового поля, обработчик изменений и обработчик отправки, который отправляет данные на маршрут API, а затем очищает поле. При реальной реализации отправляйте в запрос заголовок Content-Type: application/json, а также рассмотрите возможность использования Server Action вместо отдельного маршрута API.
Такое разделение позволяет легко понимать структуру: сервер управляет данными, а клиент — интеракцией.
Правила, обеспечивающие четкое разделение ответственностей
- В App Router компоненты по умолчанию являются Server Components.
- Добавляйте
"use client"только там, где вам нужны состояние, эффекты, обработчики событий или API браузера, такие какwindowиlocalStorage. - Server Components могут импортировать и отображать Client Components, как это делает страница с
CommentSection.
детей или других атрибутов, что позволяет вкладывать контент, отрендеренный на сервере, внутрь интерактивной оболочки."use client" также обозначает границу, а не отдельный файл: всё, что импортируется этим файлом, становится частью клиентского пакета. Размещение этой директивы как можно ниже в иерархии, на мелких интерактивных элементах, позволяет оставить остальное на сервере.
Итоги
Как только возникает вопрос «где это будет выполняться?», он становится первым вопросом, который вы задаете о компоненте, и стандартным ответом App Router является сервер.
- Компоненты сервера перемещают процесс отрисовки и доступ к данным на сервер, при этом не включают JavaScript-код компонентов.
- Загрузка данных осуществляется с помощью простых конструкций
async/awaitвнутри компонента. - Рассматривайте опцию
"use client"как возможность для интерактивных элементов, а не как стандартное поведение. - Практический первый шаг: выберите один компонент для загрузки данных в существующем проекте, проверьте, действительно ли он требует браузера, и в случае отрицательного ответа преобразуйте его.
При наличии такой модели изучение лейаутов, технологии Suspense для потоковой передачи данных, Server Actions и параллельных маршрутов становится гораздо проще, поскольку все они основаны на одной и той же концепции с сервером в центре.
Связанные материалы
- Создание надежной страницы с информацией о фильме с использованием Next.js App Router — Узнайте, как правильно загружать и кэшировать данные из API OMDB в Next.js App Router с помощью асинхронных серверных компонентов, параметров типа awaited и эффективной обработки ошибок 404.
- Server Actions или Route Handlers? Руководство по принятию решений для Next.js 16 — Узнайте, когда операции в Next.js 16 следует реализовывать с помощью Server Actions, а когда требуются Route Handlers, с исправленными примерами для форм, обработки ошибок, механизма optimism и вебхуков.