Оптимизация производительности в Next.js: от шаблонных ошибок к инженерным решениям

Серверный рендеринг (SSR) в Next.js обещает улучшение производительности и SEO, но в реальных проектах часто превращается в ловушку для неопытных разработчиков. Рассмотрим практические аспекты работы с getServerSideProps и гидратацией, которые обычно игнорируют в туториалах, но критически важны для production-приложений.

Проблема двойной загрузки данных

Типичный антипаттерн:

javascript
// Страница продукта
export async function getServerSideProps() {
  const res = await fetch('https://api/store/products/123');
  return { props: { product: await res.json() } };
}

function ProductPage({ product }) {
  const [clientSideProduct, setProduct] = useState(product);
  
  useEffect(() => {
    fetch('/api/client/product/123').then(r => r.json()).then(setProduct);
  }, []);
  
  return <div>{clientSideProduct?.price}</div>;
}

Здесь данные запрашиваются дважды: на сервере и клиенте. Решение — использовать единый источник истины:

javascript
// lib/product.js
let ssrCache = null;

export async function fetchProduct(id, context) {
  if (context?.req && !ssrCache) {
    ssrCache = await fetchInternalAPI(`/products/${id}`);
  }
  return ssrCache || fetchClientSide(`/api/product/${id}`);
}

// Страница продукта
function ProductPage({ product }) {
  const [data, setData] = useState(product);
  // Клиентские обновления через WebSocket или повторные запросы
}

Контроль гидратации компонентов

Неоптимальная гидратация приводит к "мерцанию" интерфейса. Используйте стратегическое разделение кода:

javascript
// components/Chart.client.jsx
import dynamic from 'next/dynamic';

const ClientSideChart = dynamic(
  () => import('./HighchartsWrapper'),
  { ssr: false, loading: () => <Skeleton width={300} /> }
);

Для сложных форм управляйте состоянием через провайдеры, которые различают серверную и клиентскую инициализацию:

javascript
const FormContext = createContext();

export function FormProvider({ serverData, children }) {
  const [state, setState] = useState(() => ({
    initialized: !!serverData,
    data: serverData || DEFAULT_DATA
  }));
  
  // ...
}

// В getServerSideProps:
export async function getServerSideProps() {
  return { props: { serverData: await loadFormData(req) } };
}

Оптимизация сборки через модульный CSS

Шаблонные решения с globals.css приводят к вздутию стилей. Используйте композицию CSS Modules:

css
/* components/Button.module.css */
.base {
  padding: 0.5rem 1rem;
  font: inherit;
}

.primary {
  composes: base;
  background: var(--color-primary);
}

/* В компоненте */
import styles from './Button.module.css';

export function Button({ variant }) {
  return <button className={styles[variant]} />;
}

Для критического CSS внедряйте инлайновые стили через next/head с хэшированием классов на сервере.

Работа с кэшированием API

Стратегии кэширования для getServerSideProps:

  1. Для статических данных с ревалидацией:
javascript
export async function getServerSideProps({ res }) {
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=10, stale-while-revalidate=59'
  );
  
  return { props: { data: await fetchData() } };
}
  1. Для персонализированных ответов используйте cookies-vary:
javascript
export async function getServerSideProps({ req }) {
  const session = await getSession(req);
  const cacheKey = generateCacheKey(req.url, session.userId);
  
  if (cache.has(cacheKey)) {
    return { props: cache.get(cacheKey) };
  }
  
  // ...
}

Анализ производительности в реальном времени

Интегрируйте пользовательские метрики через PerformanceObserver:

javascript
export function usePerfMetrics() {
  useEffect(() => {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      sendToAnalytics(entries.filter(e => e.name === 'Next.js-hydration'));
    });
    
    observer.observe({ type: 'longtask', buffered: true });
    return () => observer.disconnect();
  }, []);
}

// В _app.js
function MyApp() {
  usePerfMetrics();
  // ...
}

Заключение

Оптимизация Next.js-приложений требует понимания жизненного цикла рендеринга. Основные правила:

  1. Разделяйте данные для сервера и клиента через контексты
  2. Используйте стратегическое кэширование с TTL, зависящим от типа данных
  3. Контролируйте гидратацию через условие инициализации состояния
  4. Внедряйте прогрессивную загрузку для несущественных компонентов
  5. Инструментируйте ключевые метрики прямо в коде приложения

Производительность — не разовая настройка, а часть процесса разработки. Регулярный аудит с помощью lighthouse-ci и анализ реальных пользовательских метрик должны быть интегрированы в CI/CD. Для сложных сценариев рассматривайте архитектурные изменения: переход на streaming SSR или частичную статическую генерацию через ISR с fallback.