В традиционных архитектурах Single Page Application (SPA) получение данных происходило на стороне клиента с помощью хуков useEffect или клиентских библиотек вроде React Query и SWR.
Этот подход создавал серьезные проблемы с производительностью:
Next.js App Router кардинально меняет парадигму: получение данных переносится на сервер. Серверные компоненты обращаются к базам данных и микросервисам напрямую внутри локальной сети дата-центра с задержками менее 1–2 мс. Клиент получает полностью готовый HTML и поток данных, код драйверов баз данных не попадает в браузер, а секретные API-ключи остаются надёжно изолированными на бэкенде.
В Next.js App Router получение данных строится вокруг нативной функции fetch и асинхронных серверных компонентов (Async Server Components). Next.js дополняет веб-стандарт fetch двухуровневой системой кэширования:
Исследуйте прохождение сетевых запросов через Router Cache, Full Route Cache, Memoization и Data Cache
GET /catalog HTTP/1.1 (Без локального кэша)Пользователь впервые открывает страницу. В браузере нет кэша маршрутов. Запрос идет на сервер, где Full Route Cache и Data Cache проверяют наличие статических данных.
Серверный компонент вызывает fetch() или функцию доступа к базе данных.
React инспектирует память текущего рендера. При совпадении URL и параметров результат мгновенно возвращается без I/O операций.
Если в памяти пусто, Next.js проверяет постоянный кэш на диске/CDN с учётом тегов и времени revalidate.
Если кэш отсутствует или помечен как Stale, выполняется реальный сетевой запрос к источнику данных.
Ответ сохраняется в Data Cache и передается в дерево компонентов для генерации виртуального представления.
Что произойдет, если три независимых серверных компонента (Header, ProfileCard, ActivityFeed) на одной странице вызовут fetch('https://api.example.com/user', { cache: 'no-store' })?
fetch()#В Next.js 16 вызовы fetch() по умолчанию не кэшируются (cache: 'no-store'). Для включения кэширования необходимо явно задавать стратегию в опциях:
import { notFound } from 'next/navigation';
interface User {
id: number;
name: string;
email: string;
}
export default async function UsersPage() {
// В Next.js 16 fetch() по умолчанию НЕ кэшируется (no-store).
// Для включения кэша используйте 'force-cache', next: { revalidate: N } или директиву 'use cache'
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
next: {
revalidate: 3600, // Time-based ISR: кэш на 1 час
tags: ['users-list'] // Тег для сброса по требованию
}
});
if (!res.ok) {
if (res.status === 404) notFound();
throw new Error('Не удалось загрузить пользователей');
}
const users: User[] = await res.json();
return (
<main className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-4">Список пользователей</h1>
<ul className="divide-y divide-border">
{users.map((user) => (
<li key={user.id} className="py-2.5 flex justify-between">
<span className="font-medium text-text-primary">{user.name}</span>
<span className="text-text-muted">{user.email}</span>
</li>
))}
</ul>
</main>
);
}React автоматически мемоизирует одинаковые вызовы fetch внутри дерева компонентов одного рендера. Больше нет необходимости передавать данные через проп-дриллинг из Layout в Page:
// lib/api/users.ts
export async function getUser(id: string) {
// Этот запрос автоматически дедуплицируется React!
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 60 }
});
return res.json();
}
// app/users/[id]/layout.tsx (Server Component)
interface LayoutProps {
params: Promise<{ id: string }>; // В Next.js 15+ параметры строго асинхронные!
children: React.ReactNode;
}
export default async function UserLayout({ params, children }: LayoutProps) {
const { id } = await params;
const user = await getUser(id); // Запрос 1: выполняется по сети
return (
<div>
<header className="border-b border-border p-4">Пользователь: {user.name}</header>
{children}
</div>
);
}
// app/users/[id]/page.tsx (Server Component)
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getUser(id); // Запрос 2: мгновенно возвращается из Request Memoization!
return <main className="p-4">Email: {user.email}</main>;
}React.cache() для прямого доступа к базам данных#Автоматическая дедупликация работает только для fetch(). Если вы обращаетесь к базе данных напрямую через ORM (Prisma, Drizzle), оберните функцию в React.cache():
import 'server-only';
import { cache } from 'react';
import { db } from '@/lib/db';
// React.cache дедуплицирует запросы к БД в рамках одного рендера
export const getUserById = cache(async (id: string) => {
// Выполнится ровно один раз, даже если вызвать в 5 компонентах на одной странице
return await db.user.findUnique({
where: { id },
select: { id: true, name: true, email: true }
});
});React.cache() хранит данные только в памяти одного рендера. Для сохранения результатов запросов к БД между разными пользователями в постоянный Data Cache в Next.js 16 стандартом стала декларативная директива 'use cache':
import 'server-only';
import { cacheLife, cacheTag, unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
// 1. Стандарт Next.js 16: директива 'use cache' с пресетами времени жизни
export async function getPopularPosts(limit: number) {
'use cache';
cacheLife('hours'); // Пресет кэша: seconds, minutes, hours, days, max
cacheTag('posts-summary');
return await db.post.findMany({
orderBy: { views: 'desc' },
take: limit,
});
}
// 2. Обратная совместимость с Next.js 14/15: функция unstable_cache
export const getPopularPostsLegacy = unstable_cache(
async (limit: number) => {
return await db.post.findMany({
orderBy: { views: 'desc' },
take: limit,
});
},
['popular-posts-cache-key'],
{
revalidate: 3600,
tags: ['posts-summary'],
}
);Promise.all#Для предотвращения сетевых водопадов запускайте независимые асинхронные операции параллельно:
import { getPost, getComments } from '@/lib/dal/posts';
interface PageProps {
params: Promise<{ id: string }>; // Стандарт Next.js 15+
}
export default async function PostPage({ params }: PageProps) {
const { id } = await params;
// 1. Инициируем запросы параллельно БЕЗ await
const postPromise = getPost(id);
const commentsPromise = getComments(id);
// 2. Ожидаем завершения обоих запросов одновременно
const [post, comments] = await Promise.all([postPromise, commentsPromise]);
return (
<article className="p-8 max-w-3xl mx-auto space-y-6">
<h1 className="text-3xl font-bold">{post.title}</h1>
<p className="text-text-muted">{post.body}</p>
<section className="border-t border-border pt-6">
<h2 className="text-xl font-bold mb-4">Комментарии ({comments.length})</h2>
{/* Рендеринг комментариев */}
</section>
</article>
);
}use())#Если один из запросов занимает значительное время (например, аналитика или комментарии), не блокируйте весь рендеринг страницы. Передайте неразрешенный промис в клиентский компонент:
import { Suspense } from 'react';
import { getSlowAnalyticsPromise } from '@/lib/dal/analytics';
import { AnalyticsChart } from './AnalyticsChart';
export default async function DashboardPage({
params
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Запускаем медленный запрос без await
const analyticsPromise = getSlowAnalyticsPromise(id);
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Аналитика проекта #{id}</h1>
<Suspense fallback={<div className="h-64 bg-bg-elevated animate-pulse rounded-xl" />}>
{/* Передаем промис клиенту: он развернется хуком use() */}
<AnalyticsChart promise={analyticsPromise} />
</Suspense>
</div>
);
}'use client';
import { use } from 'react';
interface Metric {
date: string;
views: number;
}
export function AnalyticsChart({ promise }: { promise: Promise<Metric[]> }) {
// React 19 хук use() разворачивает потоковый промис
const data = use(promise);
return (
<div className="border border-border rounded-xl p-4 bg-bg-surface">
<div className="text-sm text-text-muted">Просмотров за период: {data.length}</div>
</div>
);
}Почему передача объектного литерала вида getUser({ id: '123' }) в функцию, обёрнутую в React.cache(), приводит к отказу мемоизации и повторным запросам к базе данных?
В Next.js 16 доступ к свойствам params и searchParams стал строго асинхронным для обеспечения совместимости с Partial Prerendering:
// ❌ СТАРОЕ ПОВЕДЕНИЕ: синхронная деструктуризация вызовет ошибку
// export default async function Page({ params }: { params: { id: string } }) {
// const user = await getUser(params.id);
// }
// ✅ СТАНДАРТ (Next.js 16):
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getUser(id);
}Функция React.cache() сравнивает аргументы по ссылке (Object.is). Если вы передадите в мемоизированную функцию новый объектный литерал { id: '123' }, для каждого вызова будет создаваться новый объект в куче, мемоизация не сработает, и запрос выполнится повторно.
Решение: В функции, оборачиваемые в React.cache(), всегда передавайте только скалярные примитивы (строки, числа, булевы флаги).
// ❌ ОШИБКА: передача объектного литерала ломает мемоизацию React.cache()
// const getUser = cache(async (options: { id: string }) => { /* ... */ });
// await getUser({ id: '123' }); // Вызов 1: объект A в куче
// await getUser({ id: '123' }); // Вызов 2: объект B в куче, кэш промах!
// ✅ ПРАВИЛЬНО: передавать только скалярные примитивы
const getUser = cache(async (id: string) => { /* ... */ });
await getUser('123'); // Мемоизация работает корректноПопытка вызвать собственный Route Handler через fetch('http://localhost:3000/api/users') из серверного компонента создает лишний TCP/HTTP запрос «сервер к самому себе», требует жесткого указания абсолютного хоста и ломается внутри изолированных контейнеров без публичного роутинга.
Решение: Выносите логику в сервисный слой (DAL) и вызывайте функцию базы данных напрямую из Server Component.
Когда компонент запрашивает запись из базы и передает её целиком в Client Component, все скрытые поля модели (пароли, токены, приватные заметки) сериализуются в открытый RSC Flight Payload.
Решение: Всегда явно фильтруйте поля на уровне запроса (select: { id: true, name: true }) либо используйте React 19 Taint API (experimental_taintObjectReference).
Создается изолированная директория lib/dal/, защищенная пакетом server-only:
// lib/dal/users.ts
import 'server-only';
import { cache } from 'react';
import { db } from '@/lib/db';
import { verifySession } from '@/lib/auth';
// 1. React.cache() гарантирует дедупликацию внутри рендера
export const getCurrentUser = cache(async () => {
// 2. Единая точка проверки сессии
const session = await verifySession();
if (!session.isAuth) return null;
// 3. Безопасная проекция: исключаем passwordHash из выборки
const user = await db.user.findUnique({
where: { id: session.userId },
select: { id: true, name: true, email: true, role: true }
});
return user;
});Теперь getCurrentUser() можно безбоязненно вызывать в layout.tsx, в page.tsx и в любом дочернем компоненте:
React.cache().'server-only'.