Серверный рендеринг (SSR) в Next.js обещает улучшение производительности и SEO, но в реальных проектах часто превращается в ловушку для неопытных разработчиков. Рассмотрим практические аспекты работы с getServerSideProps и гидратацией, которые обычно игнорируют в туториалах, но критически важны для production-приложений.
Проблема двойной загрузки данных
Типичный антипаттерн:
// Страница продукта
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>;
}
Здесь данные запрашиваются дважды: на сервере и клиенте. Решение — использовать единый источник истины:
// 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 или повторные запросы
}
Контроль гидратации компонентов
Неоптимальная гидратация приводит к "мерцанию" интерфейса. Используйте стратегическое разделение кода:
// components/Chart.client.jsx
import dynamic from 'next/dynamic';
const ClientSideChart = dynamic(
() => import('./HighchartsWrapper'),
{ ssr: false, loading: () => <Skeleton width={300} /> }
);
Для сложных форм управляйте состоянием через провайдеры, которые различают серверную и клиентскую инициализацию:
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:
/* 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:
- Для статических данных с ревалидацией:
export async function getServerSideProps({ res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
);
return { props: { data: await fetchData() } };
}
- Для персонализированных ответов используйте cookies-vary:
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:
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-приложений требует понимания жизненного цикла рендеринга. Основные правила:
- Разделяйте данные для сервера и клиента через контексты
- Используйте стратегическое кэширование с TTL, зависящим от типа данных
- Контролируйте гидратацию через условие инициализации состояния
- Внедряйте прогрессивную загрузку для несущественных компонентов
- Инструментируйте ключевые метрики прямо в коде приложения
Производительность — не разовая настройка, а часть процесса разработки. Регулярный аудит с помощью lighthouse-ci и анализ реальных пользовательских метрик должны быть интегрированы в CI/CD. Для сложных сценариев рассматривайте архитектурные изменения: переход на streaming SSR или частичную статическую генерацию через ISR с fallback.