У традиційних архітектурах Single Page Application (SPA) отримання даних відбувалося на стороні клієнта за допомогою хуків useEffect або клієнтських бібліотек на кшталт TanStack 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 15+ виклики fetch() за замовчуванням не кешуються (cache: 'no-store'). Для увімкнення кешування необхідно явно вказувати стратегію в опціях:
import { notFound } from 'next/navigation';
interface User {
id: number;
name: string;
email: string;
}
export default async function UsersPage() {
// Починаючи з Next.js 15, fetch() за замовчуванням НЕ кешується (no-store).
// Для увімкнення кешу вказуйте 'force-cache' або next: { revalidate: N }
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 }
});
});unstable_cache()#React.cache() зберігає дані лише в пам'яті одного рендеру. Для збереження результатів запитів до БД між різними користувачами у постійний Data Cache використовується unstable_cache():
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
export const getPopularPosts = unstable_cache(
async (limit: number) => {
return await db.post.findMany({
orderBy: { views: 'desc' },
take: limit,
});
},
['popular-posts-cache-key'], // Ключові частини кешу (Key parts)
{
revalidate: 3600, // Оновлювати раз на годину
tags: ['posts-summary'] // Тег для скидання через revalidateTag('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 15+ доступ до властивостей params та searchParams став асинхронним для забезпечення сумісності з Partial Prerendering:
// ❌ СТАРА ПОВЕДІНКА: синхронна деструктуризація викличе помилку
// export default async function Page({ params }: { params: { id: string } }) {
// const user = await getUser(params.id);
// }
// ✅ СТАНДАРТ (Next.js 15+):
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'.