Разработка гибридного рендеринга (SSR + CSR) для веб-приложения
Гибридный рендеринг — это когда разные части приложения или разные маршруты используют разные стратегии рендеринга в зависимости от требований. Публичные страницы с SEO рендерятся на сервере, закрытые дашборды — на клиенте, лендинги — статически. Всё это в рамках одного приложения.
Это не компромисс «взяли чуть-чуть от каждого» — это точная архитектурная настройка под требования каждой части продукта.
Модели гибридного рендеринга
По маршрутам (route-level rendering): каждый маршрут объявляет свою стратегию. Next.js, Nuxt 3 и SvelteKit поддерживают это нативно.
По компонентам (component-level rendering): Server Components рендерятся на сервере, Client Components — на клиенте. Граница проходит внутри одного маршрута.
По сегментам (segment-level rendering): шапка и навигация — статика, основной контент — SSR, виджеты (чат, уведомления) — CSR.
Конфигурация в Next.js App Router
// next.config.ts — routeRules / export config
export const revalidate = 3600; // По умолчанию: ISR с TTL 1 час
Каждый сегмент маршрута управляет рендерингом независимо:
app/
(marketing)/ # Группа без layout-влияния
page.tsx # SSG — статическая главная
about/page.tsx # SSG
blog/
page.tsx # ISR — список постов
[slug]/page.tsx # ISR — пост
(app)/
layout.tsx # Серверный layout с проверкой сессии
dashboard/
page.tsx # SSR — дашборд с реальными данными
reports/
page.tsx # CSR — тяжёлые графики, только клиент
// app/(app)/reports/page.tsx — принудительный CSR
'use client'; // Весь маршрут как клиентский компонент
import dynamic from 'next/dynamic';
// Тяжёлые библиотеки только на клиенте
const RechartsChart = dynamic(() => import('@/components/charts/RechartsChart'), {
ssr: false,
loading: () => <ChartSkeleton />,
});
Конфигурация в Nuxt 3
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Публичный сайт
'/': { prerender: true },
'/about': { prerender: true },
'/blog/**': { isr: 3600 },
// API и приложение
'/api/**': { cors: true },
'/app/**': { ssr: false }, // SPA для закрытых разделов
'/admin/**': { ssr: true }, // SSR для admin-панели (SEO не нужен, но нужна быстрая первая загрузка)
},
});
Server и Client Components — граница рендеринга
React Server Components (Next.js App Router) — ключ к гибридности на уровне компонентов:
// Серверный компонент — запрос прямо к БД, без API-round-trip
// app/products/page.tsx
import { db } from '@/lib/db';
import { ProductCard } from './product-card'; // Серверный
import { FilterBar } from './filter-bar'; // Клиентский
export default async function ProductsPage({
searchParams
}: {
searchParams: { category?: string; sort?: string }
}) {
// Этот код выполняется только на сервере
const products = await db.product.findMany({
where: { category: searchParams.category },
orderBy: { [searchParams.sort ?? 'name']: 'asc' },
include: { images: { take: 1 } },
});
return (
<div>
{/* Клиентский компонент — управляет фильтрами через URL */}
<FilterBar initialCategory={searchParams.category} />
{/* Серверный компонент — просто рендерит данные */}
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
// filter-bar.tsx — клиентский компонент
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export function FilterBar({ initialCategory }: { initialCategory?: string }) {
const router = useRouter();
const params = useSearchParams();
function setCategory(category: string) {
const newParams = new URLSearchParams(params);
newParams.set('category', category);
router.push(`?${newParams.toString()}`);
}
return (
<nav>
{CATEGORIES.map(cat => (
<button key={cat.id} onClick={() => setCategory(cat.id)}>
{cat.name}
</button>
))}
</nav>
);
}
Передача данных между Server и Client компонентами
Server Components не могут принимать функции от Client Components — только сериализуемые данные:
// Правильно: серверные данные как props клиентского компонента
export default async function Page() {
const user = await getUser(); // Серверный fetch
return <UserProfile user={user} />; // user — сериализуемый объект
}
// Неправильно: нельзя передать функцию из Server в Client
// return <Button onClick={serverFunction} /> // Ошибка компиляции
Для серверных действий (мутаций) используются Server Actions:
// actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function updateProduct(id: string, data: Partial<Product>) {
await db.product.update({ where: { id }, data });
revalidatePath(`/products/${id}`); // Инвалидировать кэш
}
// Клиентский компонент вызывает серверный экшен напрямую
'use client';
import { updateProduct } from './actions';
function EditForm({ product }: { product: Product }) {
return (
<form action={async (formData) => {
'use server'; // Встроенный серверный экшен
await updateProduct(product.id, {
name: formData.get('name') as string,
});
}}>
<input name="name" defaultValue={product.name} />
<button type="submit">Сохранить</button>
</form>
);
}
Стриминг и Suspense в гибридной архитектуре
// Параллельная загрузка независимых серверных данных
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
{/* Рендерится немедленно — не ждёт данных */}
<DashboardHeader />
{/* Каждый блок стримится независимо */}
<Suspense fallback={<StatsSkeleton />}>
<Stats /> {/* async компонент с fetch */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
Пользователь видит шапку мгновенно, блоки появляются по мере готовности данных — без блокировки всей страницы.
Метрики и компромиссы
| Стратегия | TTFB | TTI | SEO | Сложность |
|---|---|---|---|---|
| Чистый SSR | Высокий | Средний | Отлично | Средняя |
| Чистый CSR | Низкий | Высокий | Плохо | Низкая |
| Чистый SSG | Минимальный | Минимальный | Отлично | Низкая |
| Гибрид (App Router) | Низкий | Низкий | Отлично | Высокая |
Гибридный рендеринг максимально эффективен, но требует понимания модели — где проходит граница server/client, как работает кэш, как избежать waterfall запросов.
Сроки реализации
- Неделя 1–2: проектирование границ server/client по маршрутам, настройка Next.js App Router или Nuxt 3
- Неделя 3: Server Components для публичных страниц, Server Actions для мутаций
- Неделя 4: Client Components для интерактивных частей, Suspense-границы
- Неделя 5: ISR и кэширование для публичного контента, оптимизация TTFB
- Неделя 6: тестирование, мониторинг Core Web Vitals, деплой







