Розробка гібридного рендеру (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 — тільки сериалізовані дані:
// Правильно: серверні дані як пропси клієнтського компонента
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, розгортання







