SEO в Next.js: Server Components vs Client Components
Что такое RSC
React Server Components (RSC) — компоненты, которые рендерятся на сервере.
Что даёт:
- HTML на сервере.
- SEO из коробки.
- Быстрее — меньше JS.
- Доступ к БД и API.
Что нет:
useState.useEffect.- Обработчики событий.
Правило: по умолчанию — Server Component.
Client Components
Что это: компоненты с директивой 'use client'.
Что даёт:
- Интерактивность.
- State, эффекты.
- Обработчики.
- Браузерные API.
Что стоит:
- JS в бандле.
- Гидратация.
- Плохо для SEO, если злоупотреблять.
Правило: 'use client' — только там, где нужен интерактив.
Как работают
Server Component:
- Рендерится на сервере.
- Отдаёт HTML + сериализованный payload.
- Не отправляет свой код в браузер.
Client Component:
- Рендерится и на сервере (SSR), и на клиенте.
- Отправляет JS.
- Гидратируется в браузере.
Правило: Server Component лучше для SEO.
Что важно для SEO
Server:
- HTML приходит сразу.
- Контент виден поисковику.
- Метаданные работают.
- Быстрее FCP и LCP.
Client:
- HTML есть (SSR), но контент может быть пустым.
- Гидратация может не успеть.
- Поисковик видит результат после JS (через рендер).
- Медленнее.
Пример: правильный подход
Страница (Server):
// app/tracks/[slug]/page.tsx
import type { Metadata } from 'next';
import LikeButton from './LikeButton';
export async function generateMetadata({ params }) {
// ...
}
export default async function Page({ params }) {
const { slug } = await params;
const track = await getTrack(slug);
return (
<article>
<h1>{track.title}</h1>
<p>{track.description}</p>
<LikeButton trackId={track.id} />
</article>
);
}
Кнопка (Client):
// app/tracks/[slug]/LikeButton.tsx
'use client';
import { useState } from 'react';
export default function LikeButton({ trackId }: { trackId: number }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
Что здесь:
- Контент — Server Component (SEO ок).
- Интерактив — Client Component (маленький).
Правило: контент на сервере, интерактив на клиенте.
Проблема: всё в Client
Плохой пример:
// app/tracks/[slug]/page.tsx
'use client';
export default function Page() {
const [track, setTrack] = useState(null);
useEffect(() => {
fetch(`/api/tracks/${slug}`).then((r) => r.json()).then(setTrack);
}, []);
if (!track) return <div>Загрузка...</div>;
return <h1>{track.title}</h1>;
}
Что плохо:
- Контент появляется только после JS.
- Поисковик может не увидеть.
- Медленный LCP.
- Плохой SEO.
Директива 'use client'
Ставится в начало файла:
'use client';
import { useState } from 'react';
Что означает:
- Этот файл — Client Component.
- Все импорты — тоже Client.
- Нельзя вернуться к Server.
Правило: ставить как можно ниже в дереве.
Композиция
Server может рендерить Client:
// Server
export default function Page() {
return <ClientComponent />;
}
Client не может рендерить Server напрямую:
'use client';
// ❌ Не работает
import ServerComponent from './ServerComponent';
Обход через children:
// Client
'use client';
export default function ClientWrapper({ children }) {
return <div>{children}</div>;
}
// Server
export default function Page() {
return (
<ClientWrapper>
<ServerComponent />
</ClientWrapper>
);
}
Правило: Server Component через children — работает.
Метаданные
Только Server Components:
// Server
export const metadata: Metadata = { title: 'Page' };
Для Client — через родителя:
// app/client-page/page.tsx (Server)
export const metadata: Metadata = { title: 'Page' };
export default function Page() {
return <ClientInteractive />;
}
Правило: оборачивать Client в Server.
SSR и Client Components
SSR рендерит Client Components на сервере тоже.
Что это значит:
- HTML есть.
- Гидратация добавляет интерактив.
Правило: контент в Client попадает в HTML, но зависит от JS.
SEO-чек-лист
Server Components:
- Контент рендерится на сервере.
- HTML полный сразу.
- Метаданные через Metadata API.
- Работает без JS.
Client Components:
- Минимум контента.
- Обёрнуты в Server.
- Не критичны для SEO.
- Ленивая гидратация.
Динамический импорт
Для тяжёлых:
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false,
loading: () => <div>Загрузка...</div>,
});
Что даёт: не грузится в первоначальном JS.
Правило: для интерактива — ок. Для контента — плохо.
Проверка
Как проверить:
- Отключить JS в браузере.
- Открыть страницу.
- Что видно?
Хорошо: весь контент. Плохо: пусто.
Также:
- GSC → Проверка URL → Отрендеренная.
- Screaming Frog → Rendered HTML.
- curl — сырой HTML.
Что писать на Server
Server:
- Контент.
- Layouts.
- Метаданные.
- Данные из API/БД.
- Формы (для SEO).
- Списки.
- Статьи.
- Карточки.
Что на Client
Client:
- Кнопки-тогглы.
- Модалки.
- Аккордеоны.
- Слайдеры.
- Формы с интерактивом.
- Фильтры.
- Тёмная тема.
- Аналитика.
- Онлайн-чаты.
Правило: не переносить контент в Client.
Пример: статья
Правильно:
// app/blog/[slug]/page.tsx (Server)
import LikeButton from './LikeButton';
import Comments from './Comments';
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
};
}
export default async function Page({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<LikeButton postId={post.id} />
<Comments postId={post.id} />
</article>
);
}
Что здесь:
- Контент — Server.
- Like — Client.
- Comments — Client (динамика).
Практика
1. Проверьте свой проект.
2. Найдите 'use client' файлы.
3. Для каждого:
- Можно ли сделать Server?
- Что в нём интерактивного?
- Можно ли вынести интерактив?
4. Перенесите контент в Server.
5. Оставьте Client только для интерактива.
6. Отключите JS — проверьте.
7. Прогоните Lighthouse.
8. Сравните до/после.
Частые ошибки
1. Весь проект 'use client':
Плохой SEO
Решение: минимизировать.
2. Контент через useEffect:
Поисковик не видит
Решение: fetch на сервере.
3. Метаданные в Client:
Не работает
Решение: Server.
4. Server внутри Client:
Ошибка
Решение: через children.
5. dynamic для контента:
SEO страдает
Решение: только для интерактива.
6. Не проверять без JS:
Не видеть проблем
Решение: тестировать.
7. Тяжёлая гидратация:
INP плохой
Решение: меньше Client.
8. Игнорировать Lighthouse:
Плохие метрики
Решение: регулярно.
Итог
- Server — для контента и SEO.
- Client — для интерактива.
- По умолчанию — Server.
'use client'— как можно ниже.- Метаданные — только Server.
- Client оборачивать в Server.
dynamic(ssr: false)— только для интерактива.- Проверка без JS — обязательно.
- Меньше Client — лучше CWV.
- Lighthouse — регулярно.
Дальше — SEO в Next.js: Open Graph и OG-изображения.