Кешування в Next.js
Кешування в Next.js зберігає результати запитів, згенерований HTML та RSC payload на чотирьох рівнях, щоб сторінки завантажувались швидше без жодного написаного вручну кешуючого коду.
Теорія
TL;DR
- Чотири рівні: мемоізація запитів (дедуплікує під час одного рендеру), кеш даних (зберігається між запитами і деплойментами), кеш повного маршруту (зберігає HTML під час білду), кеш маршрутизатора (клієнтський, на час вкладки)
- Аналогія: кухня ресторану з підготовчими станціями (мемоізація), холодильником (кеш даних), готовими стравами на роздачі (кеш маршруту) і нотатником офіціанта (кеш маршрутизатора)
- Серверні кеші переживають деплойменти; кеш маршрутизатора зникає з закриттям вкладки
- Правило вибору:
cache: 'no-store'для живих даних, статичний режим за замовчуванням для блогів і каталогів - Головна пастка:
fetchзno-storeНЕ обходить кеш повного маршруту безdynamic = 'force-dynamic'
Швидкий приклад
// app/posts/page.tsx
// Два компоненти отримують дані з одного URL в одному рендері
// Мережевий запит відбувається рівно один раз (мемоізація запитів)
async function PostList() {
const res = await fetch('https://api.example.com/posts', { cache: 'force-cache' })
const posts = await res.json()
return <ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
}
export default async function Page() {
return (
<>
<PostList /> {/* fetch виконується тут */}
<PostList /> {/* мережевий запит не йде, дані вже є */}
</>
)
}Другий <PostList /> використовує вже отримані дані. Без додаткового HTTP-запиту. Це і є мемоізація запитів.
Чотири рівні кешування
Next.js 14+ стекує кеші в певному порядку. Розуміння того, який рівень спрацьовує першим, знімає більшість питань на співбесіді.
Мемоізація запитів працює під час одного серверного рендеру. React дедуплікує fetch-виклики з однаковим URL і опціями, зберігаючи результати в Map на час запиту. Після завершення рендеру вона скидається. Для Prisma або Drizzle, які не йдуть через fetch, використовуй React.cache() для такого ж дедублювання.
Кеш даних зберігається між запитами і між деплойментами. На Vercel він використовує глобально розподілене Edge KV-сховище; локально записується у файлову систему. Він переживає перезапуски сервера, поки не скинеш його явно через revalidateTag() або revalidatePath().
Кеш повного маршруту зберігає згенерований HTML та RSC payload для статичних маршрутів під час білду. Саме він робить статичну Next.js-сторінку миттєвою: сервер віддає файл, а не виконує обчислення. Маршрут залишається статичним, поки не викликаються динамічні функції (cookies(), headers(), searchParams) і всі запити кешуються.
Кеш маршрутизатора живе в браузері. Він зберігає RSC payload відвіданих маршрутів, роблячи повернення назад миттєвим. Також автоматично передзавантажує цілі <Link>. Термін дії спливає через 30 секунд застарілості або при закритті вкладки.
Ключова різниця: статичний та динамічний рендеринг
Статичний рендеринг: Next.js виконує сторінку під час білду (або при першому запиті для ISR), зберігає HTML і RSC payload у кеші повного маршруту і віддає цей файл для всіх наступних запитів. Підходить для блогів, сторінок товарів, маркетингових сайтів.
Динамічний рендеринг запускається при читанні cookies(), headers(), searchParams або при dynamic = 'force-dynamic'. Сервер повторно виконує дерево компонентів на кожен запит. Кеш повного маршруту не задіяний.
Але кеш даних все одно працює всередині динамічних маршрутів. Сторінка може рендеритись на кожен запит і при цьому повторно використовувати закешовані відповіді API. Це два незалежні перемикачі, і більшість команд поводиться з ними як з одним.
Коли що використовувати
- Статичний блог - стандартний
fetchабоnext: { revalidate: 3600 }(кеш даних, ISR) - Сторінка товару в магазині - кеш повного маршруту при білді,
revalidateTag('product')при зміні залишку - Дашборд користувача -
cookies()або{ cache: 'no-store' }для примусового динамічного рендеру - Адмін-панель -
export const dynamic = 'force-dynamic'на рівні сторінки - Метрики в реальному часі -
cache: 'no-store'разом зdynamic = 'force-dynamic' - A/B тест -
no-storeі кастомні заголовки для вибору варіанту на кожен запит
Як працює ревалідація
Два підходи. Часовий задає вікно застарілості:
const res = await fetch('https://api.itlead.org/problems', {
next: { revalidate: 300 } // застаріє через 5 хвилин
})Подієвий через теги дозволяє скидати кеш на вимогу:
// При отриманні даних
const res = await fetch(url, { next: { tags: ['problems'] } })
// В Server Action після запису
import { revalidateTag } from 'next/cache'
revalidateTag('problems')export const revalidate = 3600 на рівні сторінки задає запасне значення для всіх запитів. Окремі fetch-виклики можуть перевизначити його власним значенням. Найменше значення визначає, коли сторінка перегенерується.
Типові помилки
Помилка 1: no-store без force-dynamic
// Неправильно: HTML все одно кешується під час білду
export default async function Page() {
const data = await fetch('/api/live', { cache: 'no-store' })
return <div>{data.value}</div>
}
// Правильно: вказати Next.js що весь маршрут динамічний
export const dynamic = 'force-dynamic'
export default async function Page() {
const data = await fetch('/api/live', { cache: 'no-store' })
return <div>{data.value}</div>
}fetch з no-store пропускає кеш даних. Але кеш повного маршруту все одно зберігає HTML під час білду. Потрібні обидва налаштування.
Помилка 2: revalidate: 0 в розрахунку що це скидає все
revalidate: 0 впливає лише на кеш даних. Мемоізація запитів і кеш повного маршруту все одно спрацьовують. Щоб повністю вийти з кешування, використовуй cache: 'no-store' разом з dynamic = 'force-dynamic'.
Помилка 3: застарілі дані після мутації через кеш маршрутизатора
Користувач відправляє форму, Server Action оновлює базу, але при наступній навігації показуються старі дані. Браузерний кеш маршрутизатора видав RSC payload до мутації. Виправлення:
import { revalidatePath } from 'next/cache'
export async function updateProfile(formData: FormData) {
'use server'
await db.user.update(...)
revalidatePath('/profile') // скидає і серверний, і клієнтський кеш
}Або виклик router.refresh() на клієнті після мутації.
Помилка 4: різні опції fetch ламають мемоізацію
Мемоізація прив'язана до URL, опцій і тіла запиту. Два виклики до одного URL з різними значеннями cache вважаються двома окремими запитами. Стандартизуй опції між компонентами, що використовують одне джерело даних.
Помилка 5: очікування що next dev використовує кеш повного маршруту
Режим розробки повністю обходить кеш повного маршруту, щоб кожна зміна одразу відображалась. Одного разу витратив хвилин тридцять на відладку уявного "баґу кешування" в dev, поки не згадав: кеш повного маршруту існує лише в продакшені. Завжди перевіряй поведінку кешування через next build && next start.
Де зустрічається в реальних проектах
- Vercel Commerce (nextjs-commerce): кеш повного маршруту для сторінок товарів, кеш даних з
revalidateTagдля залишків - Supabase + Next.js стартери:
revalidate: 60для списків користувачів,no-storeдля сторінок редагування - Next.js дашборди з NextAuth: читання
headers()примушує динамічний рендер для авторизованих маршрутів - Порівняно з React Query: Next.js кешування обробляє серверні запити без додаткового коду; React Query краще підходить для клієнтських мутацій та оптимістичних оновлень
Питання з продовженням
Q: Яка різниця між кешем даних і кешем повного маршруту?
A: Кеш даних зберігає сирі відповіді fetch (JSON) між запитами. Кеш повного маршруту зберігає згенерований HTML та RSC payload для всього статичного маршруту під час білду.
Q: Якщо встановити export const revalidate = 3600 на рівні сторінки, що відбувається з окремими fetch-викликами з власним revalidate?
A: Значення рівня сторінки є запасним. Будь-який fetch з власним next: { revalidate } перевизначає його для конкретного виклику. Найменше значення визначає, коли сторінка перегенерується.
Q: Хард-рефреш проти м'якої навігації: що змінюється?
A: М'яка навігація (клік на <Link>) спочатку звертається до кешу маршрутизатора. Хард-рефреш повністю пропускає його, йде на сервер, який перевіряє кеш повного маршруту і кеш даних.
Q: Чи читання searchParams завжди робить маршрут динамічним?
A: Так. Як тільки Next.js бачить звернення до searchParams, маршрут виходить з кешу повного маршруту. Сторінка рендериться на кожен запит, але кеш даних все одно працює для запитів всередині неї.
Q (senior): Edge runtime проти Node.js runtime: як поводиться кешування?
A: На Vercel Edge runtime використовує глобально розподілене KV-сховище, тому закешовані дані спільні між усіма регіонами. Node.js runtime використовує локальну файлову систему, тому кожен серверний інстанс має власний кеш. Це важливо для мультирегіональних деплойментів, де час інвалідації кешу відрізняється.
Q (senior): Чому next dev ніколи не показує кеш повного маршруту навіть для статичних сторінок?
A: Режим розробки навмисно обходить його, щоб кожна зміна одразу відображалась. Крок білду (next build) потрібен для заповнення і тестування кешу повного маршруту. Це типова причина, чому поведінка в dev не збігається з продакшеном.
Приклади
Базовий: мемоізація запитів через React.cache
// lib/data.ts
import { cache } from 'react'
import { db } from '@/lib/db'
// Для ORM-запитів, які не йдуть через fetch
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } })
})
// app/profile/[id]/page.tsx
import { getUser } from '@/lib/data'
async function Avatar({ id }: { id: string }) {
const user = await getUser(id) // DB-запит відбувається один раз
return <img src={user.avatar} alt={user.name} />
}
export default async function Page({ params }: { params: { id: string } }) {
const user = await getUser(params.id) // той самий виклик, результат вже є
return (
<div>
<h1>{user.name}</h1>
<Avatar id={params.id} />
</div>
)
}
// Результат: один запит до БД для обох компонентів у тому ж рендеріReact.cache() огортає функцію так, щоб повторні виклики з тими ж аргументами повертали закешований результат. Скидається на кожен запит, тому між користувачами дані не змішуються.
Середній: ISR-сторінка товару з тегованою ревалідацією
// app/products/[slug]/page.tsx
export default async function ProductPage({
params
}: {
params: { slug: string }
}) {
const res = await fetch(`https://api.example.com/products/${params.slug}`, {
next: {
revalidate: 3600, // мінімум щогодинна ревалідація
tags: [`product-${params.slug}`] // або на вимогу через revalidateTag
}
})
const product = await res.json()
return (
<div>
<h1>{product.name}</h1>
<p>Залишок: {product.stock}</p>
<p>{product.price} грн</p>
</div>
)
}
// app/actions/updateStock.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateStock(slug: string, newStock: number) {
await fetch(`https://api.example.com/products/${slug}`, {
method: 'PATCH',
body: JSON.stringify({ stock: newStock })
})
revalidateTag(`product-${slug}`) // оновлюється лише сторінка цього товару
}Сторінка віддає закешований HTML поки не мине година або не спрацює updateStock. Все інше залишається в кеші. Це стандартний ISR-патерн для більшості ecommerce-проектів.
Просунутий: динамічний маршрут з частковим кешуванням
// app/dashboard/page.tsx
// Маршрут динамічний (читає cookies для авторизації),
// але публічний fetch все одно кешується
import { cookies } from 'next/headers'
export default async function Dashboard() {
// Читання cookies робить маршрут динамічним
const session = cookies().get('session-token')
// Цей fetch все одно використовує кеш даних (5 хвилин)
const statsRes = await fetch('https://api.example.com/stats', {
next: { revalidate: 300 }
})
// А цей оновлюється на кожен запит
const userRes = await fetch(`/api/user/${session?.value}`, {
cache: 'no-store'
})
const user = await userRes.json()
const stats = await statsRes.json()
return (
<div>
<h1>Вітаємо, {user.name}</h1>
<p>Всього користувачів: {stats.total}</p> {/* з кешу, актуальність 5 хв */}
</div>
)
}
// Результат: сторінка рендериться на кожен запит, дані користувача свіжі,
// але статистика береться з кешу і пропускає API-запит більшість часуЧитання cookies() робить маршрут динамічним. Але окремі fetch-виклики всередині нього все одно використовують кеш даних, якщо не передати no-store. Динамічний рендеринг і кешування даних - це два незалежні перемикачі. Комбінування обох дозволяє отримувати свіжі дані користувача поряд із закешованими спільними даними в одному рендері.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.