Розроблення SSR (Server-Side Rendering) для веб-приложення
Server-Side Rendering — це коли сервер генерує повний HTML перед відправкою браузеру. Користувач бачить контент одразу, без чекання завантаження JS-бандла та виконання клієнтського рендеру. Для SEO, для першої завантаження на повільних з'єднаннях, для доступності — SSR дає вимірні переваги перед чисто клієнтським приложенням.
Але SSR — це не кнопка. Це архітектурне рішення з компромісами, які потрібно розуміти та правильно балансувати.
Моделі SSR та їхнє застосування
Повний SSR (традиційний): кожен запит → серверний рендер → повний HTML-відповідь. Немає стану на клієнті до гідратації.
SSR з гідратацією: сервер рендерит HTML, клієнт завантажує той же JS-код та «оживляє» статичний HTML — прикріплює события, відновлює стан.
Streaming SSR: HTML відправляється браузеру по мері готовності частин сторінки, не чекаючи повного рендеру. Перші байти досягають браузера швидше.
SSR з кешуванням: результат рендеру кешується на заданий час — сервер не рендерит одне й те саме при кожному запиті.
Реалізація на Next.js (React)
Next.js — найзріліший SSR-фреймворк для React. App Router (Next.js 13+) пропонує React Server Components як модель за замовчуванням:
// app/products/[id]/page.tsx — серверний компонент
import { notFound } from 'next/navigation';
interface Props {
params: { id: string };
}
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 60 }, // ISR: кєш 60 секунд
});
if (!res.ok) return null;
return res.json() as Promise<Product>;
}
export default async function ProductPage({ params }: Props) {
const product = await getProduct(params.id);
if (!product) notFound();
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductActions productId={product.id} /> {/* Клієнтський компонент */}
</article>
);
}
// Метаданні для SEO — теж серверні
export async function generateMetadata({ params }: Props) {
const product = await getProduct(params.id);
return {
title: product?.name ?? 'Продукт не знайдено',
description: product?.description,
openGraph: { images: [product?.image] },
};
}
// app/products/[id]/product-actions.tsx — клієнтський компонент
'use client';
import { useState } from 'react';
export function ProductActions({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false);
async function addToCart() {
setLoading(true);
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId }),
});
setLoading(false);
}
return (
<button onClick={addToCart} disabled={loading}>
{loading ? 'Додаємо...' : 'До корзини'}
</button>
);
}
Реалізація на Nuxt 3 (Vue)
<!-- pages/products/[id].vue -->
<script setup lang="ts">
const route = useRoute();
const { data: product, error } = await useFetch<Product>(
`/api/products/${route.params.id}`,
{ key: `product-${route.params.id}` }
);
if (error.value || !product.value) {
throw createError({ statusCode: 404 });
}
useSeoMeta({
title: product.value.name,
description: product.value.description,
ogImage: product.value.image,
});
</script>
<template>
<article>
<h1>{{ product.name }}</h1>
<p>{{ product.description }}</p>
<ClientOnly>
<ProductActions :product-id="product.id" />
</ClientOnly>
</article>
</template>
Гідратація: підводні камені
Hydration mismatch — найчастіша проблема SSR. Якщо серверний та клієнтський HTML різні, React/Vue виправляють попередження або повністю перерендеривают компонент:
// Проблема: new Date() дає різний результат на сервері та клієнті
function LastUpdated() {
return <span>{new Date().toLocaleString()}</span>; // Mismatch!
}
// Рішення: suppressHydrationWarning для динамічних значень
function LastUpdated({ timestamp }: { timestamp: string }) {
return (
<time suppressHydrationWarning dateTime={timestamp}>
{new Date(timestamp).toLocaleString()}
</time>
);
}
Для браузер-залежного коду (localStorage, window.innerWidth) — відкладений рендер:
'use client';
import { useState, useEffect } from 'react';
function ThemeToggle() {
const [theme, setTheme] = useState<string | null>(null);
useEffect(() => {
setTheme(localStorage.getItem('theme') ?? 'light');
}, []);
if (!theme) return null; // Не рендеримо до монтування
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}
Кєшування на рівні сервера
// lib/cache.ts — Redis-кєш для тяжких запитів
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
export async function cachedFetch<T>(
key: string,
fetcher: () => Promise<T>,
ttl = 300 // секунди
): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const data = await fetcher();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
// Використання у серверному компоненті
const categories = await cachedFetch(
'categories:all',
() => db.category.findMany({ orderBy: { name: 'asc' } }),
3600
);
Метрики та мониторинг
SSR вводить серверну латентність у ланцюг рендеру. Важливо відстежувати:
| Метрика | Цільове значення |
|---|---|
| TTFB (Time to First Byte) | < 200ms |
| LCP (Largest Contentful Paint) | < 2.5s |
| FCP (First Contentful Paint) | < 1.8s |
| Серверний рендер p95 | < 500ms |
Інструменти: Vercel Analytics, Sentry Performance, OpenTelemetry + Jaeger для трейсингу серверних запитів.
Розгортання та інфраструктура
- Vercel / Netlify — автоматичний розгортання Next.js/Nuxt, Edge Runtime для низької латентності
- Node.js на VPS — через PM2 або Docker, потрібен реверс-прокси (Nginx)
- Cloudflare Workers — тільки edge-сумісний код (немає Node.js API)
- AWS App Runner / ECS — для enterprise з вимогами до ізоляції
Терміни реалізації
- Тижні 1–2: вибір стеку (Next.js/Nuxt/SvelteKit), налаштування роутингу, серверні компоненти для статичних сторінок
- Тиждень 3: динамічні маршрути, серверні обробники даних, SEO-метаданні
- Тиждень 4: клієнтські компоненти для інтерактивних частин, вирішення hydration mismatches
- Тиждень 5: кєшування (Redis/in-memory), оптимізація TTFB
- Тиждень 6: нагрузочне тестування, налаштування мониторингу, розгортання







