Server-Side Rendering (SSR) стал стандартом для современных веб-приложений, обещая улучшенный SEO и мгновенный рендеринг первого контента. Но разработка эффективного SSR-решения — это не просто включение флага в конфигурации. Рассмотрим практические аспекты реализации SSR в Next.js с акцентом на управление данными и распространенные ловушки.
Архитектурный компромисс: когда SSR действительно нужен
Прежде чем внедрять SSR, проверьте требования:
- Критичен ли немедленный отклик для первых 500 мс? (например, медиа-порталы)
- Требует ли контент синхронизации с внешними API перед рендерингом? (финансовые платформы)
- Будет ли JavaScript долго инициализироваться на клиенте? (сложные панели аналитики)
Пример целевой архитектуры в Next.js:
export async function getServerSideProps(context) {
const [userData, productList] = await Promise.all([
fetchAuthData(context.req),
fetchProductCatalog()
]);
return {
props: {
user: userData,
products: productList.items.filter(item => !item.isArchived)
}
};
}
function CatalogPage({ user, products }) {
// Клиентский код получает готовые данные из props
}
Асинхронная дилемма: как избежать waterfall-запросов
Типичная ошибка — последовательные вызовы в getServerSideProps
:
// Плохая практика - waterfall запросов
const profile = await fetchUserProfile(params.id); // 200ms
const orders = await fetchUserOrders(profile.id); // 300ms
const recommendations = await fetchRecommendations(orders[0].id); // 250ms
Решение — параллелизация с контролем ошибок:
const [profileRes, ordersRes] = await Promise.allSettled([
fetchUserProfile(params.id),
fetchUserOrdersCacheFirst(params.id)
]);
if (profileRes.status === 'rejected') {
return { notFound: true };
}
const recommendations = profileRes.value.isPremium
? await fetchPremiumRecommendations()
: await defaultRecommendations;
Жизненный цикл гидратации: почему ломается интерактивность
Серверный рендеринг не заменяет клиентскую логику. Частая ошибка — предположение, что window
доступен на сервере:
// Сломается при SSR
function GeoWidget() {
const [location, setLocation] = useState(
window.navigator.geolocation // ReferenceError
);
// ...
}
Правильный подход с учетом гидратации:
function GeoWidget() {
const [location, setLocation] = useState(null);
useEffect(() => {
const geo = navigator.geolocation.watchPosition(pos => {
setLocation(pos.coords);
});
return () => navigator.geolocation.clearWatch(geo);
}, []);
if (typeof window === 'undefined') {
return <div className="geo-skeleton" />; // Серверный фолбэк
}
// Клиентский рендеринг
}
Кеширование на уровне запросов: за пределами CDN
При динамическом SSR кеширование данных становится критичным. Пример реализации стратегии stale-while-revalidate в Next.js:
// lib/cache.js
const swrCache = new Map();
export async function cachedFetch(key, fetcher, ttl = 60) {
const record = swrCache.get(key);
if (record && Date.now() < record.expires) {
return record.data;
}
if (record?.stalePromise) {
return await record.stalePromise;
}
const stalePromise = fetcher()
.then(data => {
swrCache.set(key, {
data,
expires: Date.now() + ttl * 1000,
stalePromise: null
});
return data;
});
swrCache.set(key, { ...record, stalePromise });
return record?.data ?? await stalePromise;
}
Использование в getServerSideProps
:
export async function getServerSideProps() {
const posts = await cachedFetch('latest-posts', () =>
fetch('https://api/posts?limit=10')
);
return { props: { posts } };
}
Война с CLS: стабильность верстки при гидратации
Совету по Cumulative Layout Shift (CLS) часто игнорируют в SSR. Пример опасного кода:
function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
<!-- Допустим, image.size вычисляется асинхронно -->
<img
src={product.image.url}
width={product.image.width} // undefined при SSR
height={product.image.height}
/>
</div>
);
}
Решение с сохранением стабильности:
// components/ProductImage.js
function ProductImage({ src, size }) {
const [dimensions, setDimensions] = useState(
size ? { width: size.w, height: size.h } : null
);
useEffect(() => {
if (!size) {
const img = new Image();
img.onload = () => {
setDimensions({
width: img.naturalWidth,
height: img.naturalHeight
});
};
img.src = src;
}
}, [src, size]);
return (
<div
style={{
aspectRatio: dimensions
? `${dimensions.w}/${dimensions.h}`
: '16/9',
position: 'relative'
}}
>
<img
src={src}
style={{
position: 'absolute',
width: '100%',
height: '100%',
objectFit: 'cover'
}}
/>
</div>
);
}
Диагностика проблем: метрики SSR в продакшене
Инструментирование критично для SSR-приложений. Пример сбора показателей в Next.js:
// pages/_document.js
export function getInitialProps(ctx) {
const start = Date.now();
const props = await Document.getInitialProps(ctx);
const end = Date.now();
const serverTiming = [
`renderPage;dur=${end - start}`,
`propsSize;desc="Props size";byte=${JSON.stringify(props).length}`
].join(', ');
ctx.res.setHeader('Server-Timing', serverTiming);
return props;
}
// middleware/analytics.js
export function trackSSRMetrics(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const memoryUsed = process.memoryUsage().heapUsed / 1024 / 1024;
logToInflux({
route: req.url,
duration,
memory: memoryUsed,
status: res.statusCode
});
});
next();
}
SSR — это не серебряная пуля, но мощный инструмент при правильной реализации. Ключевые факторы успеха: стратегическое кеширование, контроль над водопадами запросов и тщательная проверка гидратации. Следующий шаг эволюции — исследовать React Server Components, где модель данных становится частью компонентной архитектуры.