Оптимизация Server-Side Rendering в Next.js: От Basic Implementation к Production-Ready

Server-Side Rendering (SSR) в Next.js часто преподносится как «просто добавь getServerSideProps», но реальные проекты сталкиваются с граблями, которые не видны в документации. Рассмотрим, как превратить базовую реализацию SSR в оптимальное решение, готовое для продакшена.


Проблема: Наивная реализация SSR
Типичный пример:

jsx
export async function getServerSideProps(context) {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();
  return { props: { data } };
}

Этот код нарушает три ключевых принципа production-решений:

  1. Нет обработки ошибок
  2. Отсутствие контроля времени выполнения
  3. Игнорирование кэширования

1. Обработка ошибок за пределами try/catch

Для SSR критически важно гарантировать рендеринг хотя бы базовой структуры страницы при сбоях API. Решение — двухуровневая обработка:

jsx
async function fetchDataWithFallback(url, fallback = {}) {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 3000);
    
    const res = await fetch(url, { signal: controller.signal });
    if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
    
    clearTimeout(timeoutId);
    return await res.json();
  } catch (error) {
    console.error(`Data fetch failed: ${error.message}`);
    return fallback;
  }
}

Использование в getServerSideProps:

jsx
export async function getServerSideProps() {
  const [mainData, secondaryData] = await Promise.all([
    fetchDataWithFallback(API_ENDPOINTS.main, DEFAULT_MAIN_DATA),
    fetchDataWithFallback(API_ENDPOINTS.secondary, {}),
  ]);

  return { 
    props: { 
      mainData,
      secondary: secondaryData?.results ?? [],
    }
  };
}

2. Контроль времени выполнения

При использовании SSR с TTV (Time-To-Visual) больше 2 секунд вы рискуете получить penalization в SEO. Для мониторинга добавьте кастомные метрики:

jsx
// pages/_app.js
export function reportWebVitals(metric) {
  if (metric.name === 'Next.js-ssr') {
    analytics.track('SSR_DURATION', {
      duration: metric.value,
      path: window.location.pathname,
    });
  }
}

Оптимизируйте тяжелые операции:

  • Выносите не критичные для первого рендера вычисления в клиентские эффекты
  • Используйте потоковую передачу данных с react-server-dom-webpack
  • Для сложных преобразований данных применяйте Web Workers в Node.js:
jsx
// server/worker.js
const { Worker } = require('worker_threads');

async function processDataInWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./data-processor.js', {
      workerData: data
    });
    
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

3. Стратегии кэширования

Используйте многоуровневое кэширование для уменьшения нагрузки на сервер:

Уровень 1: In-memory cache (Node.js process)

jsx
const LRU = require('lru-cache');

const ssrCache = new LRU({
  max: 100,
  maxAge: 1000 * 60 * 5, // 5 минут
});

export async function getCachedData(key, fetchFn) {
  if (ssrCache.has(key)) {
    return ssrCache.get(key);
  }
  
  const data = await fetchFn();
  ssrCache.set(key, data);
  return data;
}

Уровень 2: Redis для кластера серверов

jsx
const redis = require('redis');
const client = redis.createClient();

async function getOrSetCache(key, fetchFn, ttl = 300) {
  const cached = await client.get(key);
  if (cached) return JSON.parse(cached);
  
  const freshData = await fetchFn();
  await client.setex(key, ttl, JSON.stringify(freshData));
  return freshData;
}

Уровень 3: CDN Cache-Control

jsx
export async function getServerSideProps({ res }) {
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=60, stale-while-revalidate=300'
  );
  
  // ...data fetching
}

4. Сессионные данные и авторизация

Ошибка: Передача чувствительных данных через props приводит к утечкам в клиентский код. Решение — разделение данных:

jsx
// server/api/auth.js
export function getServerSideUser(context) {
  const session = getSession(context.req);
  return session?.user || null;
}

// pages/profile.js
export async function getServerSideProps(context) {
  const user = getServerSideUser(context);
  
  return {
    props: {
      // Публичные данные
      profile: await fetchPublicProfile(user?.id),
      // Приватные данные передаются через контекст SSR
      __secure: {
        billingInfo: user ? await fetchBillingInfo(user.id) : null
      }
    }
  };
}

// components/SecureDataBridge.js
import { useEffect } from 'react';
import { useSecureData } from '../context/secure-data';

export default function SecureDataBridge({ __secure }) {
  const { setSecureData } = useSecureData();
  
  useEffect(() => {
    setSecureData(__secure);
  }, []);
  
  return null;
}

Production Checklist
Перед запуском SSR в продакшене убедитесь в:

  1. Наличии circuit breakers для внешних API
  2. Реализации gracefull degradation при частичных сбоях данных
  3. Настройке мониторинга TTV и TTI
  4. Использовании изолированных контейнеров для обработки тяжелых задач
  5. Тестировании поведения при 5XX ошибках бэкенда

SSR в Next.js перестает быть «волшебной таблеткой» в сложных сценариях, но сознательное проектирование потоков данных и обработки ошибок делает его надежным фундаментом для высоконагруженных приложений. Главное правило: рассматривайте SSR не как фичу фреймворка, а как отдельный сервис со своими SLA и требованиями к инфраструктуре.