Пошук уроків, статей та іншого контенту
Поєднайте кешовані й динамічні частини сторінки через Partial Prerendering і Cache Components.
Partial Prerendering (PPR) дає змогу віддати частину сторінки одразу, а динамічні фрагменти дорендерити пізніше та передати їх через streaming.
Наприклад, сторінка товару може містити:
заголовок і структуру сторінки, які можна підготувати заздалегідь;
інформацію про товар із кешу;
кількість товарів у кошику, яка залежить від cookie поточного користувача;
повільний персоналізований блок.
Замість того щоб чекати на всі частини сторінки, Next.js:
формує доступний для кешування shell;
вставляє в нього fallback для динамічних частин;
надсилає готові фрагменти через streaming, коли вони стають доступними.
У сучасному Next.js цей підхід реалізується через Cache Components і директиву "use cache".
У старіших версіях Next.js PPR вмикався через експериментальну опцію
experimental.ppr. У версіях із Cache Components основним способом єcacheComponents: true.
Увімкніть функціональність у next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;Після цього Next.js зможе розділяти компоненти на:
кешовані;
динамічні;
компоненти, які очікують асинхронні дані.
Кешування вмикається на рівні функції або компонента за допомогою директиви:
"use cache";Директива має бути першою інструкцією у функції або на початку модуля.
Створімо функцію, яка отримує дані товару. Для прикладу вона імітує повільне звернення до бази даних.
// app/products/[id]/ProductInfo.tsx
import { cacheTag } from "next/cache";
type ProductInfoProps = {
productId: string;
};
type Product = {
id: string;
name: string;
price: number;
};
async function getProduct(productId: string): Promise<Product> {
// Імітуємо повільний запит до бази даних
await new Promise((resolve) => setTimeout(resolve, 800));
return {
id: productId,
name: `Товар ${productId}`,
price: 1299,
};
}
export async function ProductInfo({ productId }: ProductInfoProps) {
"use cache";
cacheTag(`product:${productId}`);
const product = await getProduct(productId);
return (
<section>
<h1>{product.name}</h1>
<p>Ціна: {product.price} грн</p>
</section>
);
}У цьому прикладі:
ProductInfo є асинхронним кешованим компонентом;
аргумент productId входить у ключ кешу;
різні товари кешуються окремо;
cacheTag дає змогу групувати запис кешу за тегом.
Кешувати потрібно результат, який можна безпечно повторно використовувати для різних запитів. Дані, що залежать від конкретного користувача або поточного запиту, не повинні потрапляти в такий компонент.
Тепер створімо компонент, який читає cookie. Його результат залежить від конкретного користувача, тому кешувати його не можна.
// app/products/[id]/CartStatus.tsx
import { cookies } from "next/headers";
export async function CartStatus() {
const cookieStore = await cookies();
const value = cookieStore.get("cart-count")?.value ?? "0";
const cartCount = Number(value);
return (
<aside>
<strong>Кошик:</strong> {cartCount} товарів
</aside>
);
}cookies() — динамічний API. Його значення може бути різним для кожного запиту, тому компонент із цим API повинен залишатися динамічним.
Suspense визначає межу, за якою Next.js може створити динамічний фрагмент.
// app/products/[id]/page.tsx
import { Suspense } from "react";
import { CartStatus } from "./CartStatus";
import { ProductInfo } from "./ProductInfo";
type ProductPageProps = {
params: Promise<{
id: string;
}>;
};
function ProductSkeleton() {
return (
<section aria-busy="true">
<h1>Завантаження товару...</h1>
<p>Отримуємо дані.</p>
</section>
);
}
function CartSkeleton() {
return (
<aside aria-busy="true">
Завантаження кошика...
</aside>
);
}
export default async function ProductPage({
params,
}: ProductPageProps) {
const { id } = await params;
return (
<main>
<header>
<p>Каталог</p>
<h2>Сторінка товару</h2>
</header>
<Suspense fallback={<ProductSkeleton />}>
<ProductInfo productId={id} />
</Suspense>
<Suspense fallback={<CartSkeleton />}>
<CartStatus />
</Suspense>
</main>
);
}Під час відкриття /products/42 результат може формуватися так:
Next.js готує структуру сторінки та fallback-компоненти.
Браузер одразу отримує доступний shell.
Після завершення кешованого запиту з’являється ProductInfo.
Після читання cookie з’являється CartStatus.
React замінює fallback відповідним фрагментом без повного перезавантаження сторінки.
Кожна межа Suspense є окремою точкою, у якій можна показати fallback і передати результат пізніше.
Директива "use cache" кешує результат виконання функції або компонента з урахуванням його аргументів і коду.
Наприклад:
async function ProductInfo({ productId }: { productId: string }) {
"use cache";
const product = await getProduct(productId);
return <h1>{product.name}</h1>;
}Виклики з різними аргументами мають різні записи:
ProductInfo("42") → запис для товару 42
ProductInfo("43") → запис для товару 43Аргументи кешованого компонента мають бути серіалізованими значеннями, які можна використовувати для побудови ключа кешу. Не слід передавати туди об’єкти, що містять стан конкретного запиту або користувача.
Динамічні значення потрібно отримати за межами кешованого компонента та передати явно, якщо це безпечно.
Наприклад, ідентифікатор мови може бути аргументом кешованої функції:
type ProductTitleProps = {
productId: string;
locale: "uk" | "en";
};
export async function ProductTitle({
productId,
locale,
}: ProductTitleProps) {
"use cache";
const product = await getProduct(productId);
const title =
locale === "uk"
? product.name
: `Product ${product.id}`;
return <h1>{title}</h1>;
}Натомість cookie або заголовки запиту не слід читати всередині "use cache":
// Неправильний підхід
async function UserGreeting() {
"use cache";
const cookieStore = await cookies();
// Значення залежить від користувача, але компонент оголошений кешованим
}Правильна межа виглядає так:
import { cookies } from "next/headers";
async function UserGreeting() {
const cookieStore = await cookies();
const userName = cookieStore.get("user-name")?.value ?? "Гість";
return <Greeting name={userName} />;
}
async function Greeting({ name }: { name: string }) {
"use cache";
return <p>Вітаємо, {name}!</p>;
}У цьому прикладі персональне значення читається динамічно, а компонент Greeting кешується за переданим значенням.
Теги дають змогу пов’язати кешований результат із певним типом даних:
import { cacheTag } from "next/cache";
async function ProductInfo({ productId }: { productId: string }) {
"use cache";
cacheTag(`product:${productId}`);
const product = await getProduct(productId);
return <h1>{product.name}</h1>;
}Якщо дані товару змінюються, відповідний запис можна інвалідувати в серверній логіці, наприклад після оновлення товару:
"use server";
import { revalidateTag } from "next/cache";
export async function updateProduct(productId: string) {
// Тут відбувається оновлення товару в базі даних
revalidateTag(`product:${productId}`, "max");
}Після інвалідації наступний запит отримає актуальні дані та сформує новий кешований результат.
Тег повинен бути однаковим під час запису й інвалідації:
cacheTag("product:42")
revalidateTag("product:42", "max")Динамічний компонент бажано обгортати в окремий Suspense, а не блокувати всю сторінку:
<Suspense fallback={<ProductSkeleton />}>
<ProductInfo productId={id} />
</Suspense>
<Suspense fallback={<CartSkeleton />}>
<CartStatus />
</Suspense>Так компоненти можуть завершуватися незалежно один від одного.
Якщо розмістити всі запити в одному компоненті без окремих меж:
<Suspense fallback={<PageSkeleton />}>
<ProductInfo productId={id} />
<CartStatus />
</Suspense>то fallback може залишатися на екрані, доки не завершаться обидві операції. Це не завжди помилка, але зменшує перевагу паралельного streaming.
Ці поняття пов’язані, але не повністю тотожні:
PPR описує результат: одна сторінка складається з попередньо підготовленої та динамічної частин.
Cache Components — сучасний механізм Next.js, який визначає, які частини можна кешувати, а які потрібно виконувати під час запиту.
Suspense визначає місце, у якому динамічна частина може бути відокремлена від shell.
У старому підході можна було зустріти конфігурацію:
const nextConfig = {
experimental: {
ppr: true,
},
};
export default nextConfig;Для проєктів, які використовують Cache Components, потрібно застосовувати:
const nextConfig = {
cacheComponents: true,
};
export default nextConfig;Не слід змішувати стару експериментальну конфігурацію PPR із новим способом без вимоги конкретної версії Next.js.
Небезпечно кешувати компонент, який читає cookie, заголовок або інший персональний контекст:
async function AccountPanel() {
"use cache";
const cookieStore = await cookies();
// Дані одного користувача можуть стати доступними іншому
}Перенесіть читання динамічних даних у некешований компонент.
Якщо динамічний компонент не має відповідної межі Suspense, він може блокувати підготовку сторінки або спричинити помилку під час перевірки маршруту.
Додавайте явний fallback:
<Suspense fallback={<Loading />}>
<DynamicComponent />
</Suspense>Не потрібно кешувати всю сторінку лише тому, що більшість даних статичні. Якщо всередині кешованої межі є персональний стан, кеш може повертати неправильний результат.
Кешована межа повинна містити лише дані, які безпечно повторно використовувати.
До динамічних API належать, зокрема:
cookies();
headers();
значення, що безпосередньо залежать від поточного запиту.
Отримуйте їх у динамічному батьківському компоненті та передавайте в кешовану частину лише як явні аргументи.
Окрема межа для кожного маленького текстового елемента ускладнює структуру та може створити непослідовний інтерфейс. Межу варто ставити навколо логічного блоку, який може завантажуватися незалежно.
Partial Prerendering поєднує швидкий shell із динамічними фрагментами.
У сучасному Next.js для цього використовуються Cache Components.
cacheComponents: true вмикає відповідну модель кешування.
"use cache" позначає функцію або компонент як кешований.
cookies() та інші динамічні API не повинні виконуватися всередині кешованої межі.
Suspense відокремлює динамічні фрагменти та дає змогу передавати їх через streaming.
cacheTag і revalidateTag допомагають інвалідувати пов’язані кешовані дані.
Кешуйте лише ті дані, які безпечно повторно використовувати між запитами.