Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Кешування в Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Кешування в Next.js** працює на чотирьох рівнях: мемоізація запитів (дедуплікує однакові fetch в одному рендері), кеш даних (зберігає відповіді між запитами і деплойментами), кеш повного маршруту (зберігає HTML під час білду для статичних маршрутів) і кеш маршрутизатора (RSC payload на стороні клієнта, на час вкладки). ```tsx // Кеш даних: ревалідація щогодини const res = await fetch(url, { next: { revalidate: 3600 } }) // Динамічний маршрут: без кешування HTML export const dynamic = 'force-dynamic' ``` **Головне:** `fetch` з `cache: 'no-store'` пропускає кеш даних, але не кеш повного маршруту. Для відключення кешування HTML потрібен `dynamic = 'force-dynamic'`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Кешування в Next.js** зберігає результати запитів, згенерований HTML та RSC payload на чотирьох рівнях, щоб сторінки завантажувались швидше без жодного написаного вручну кешуючого коду. ## Теорія ### TL;DR - Чотири рівні: мемоізація запитів (дедуплікує під час одного рендеру), кеш даних (зберігається між запитами і деплойментами), кеш повного маршруту (зберігає HTML під час білду), кеш маршрутизатора (клієнтський, на час вкладки) - Аналогія: кухня ресторану з підготовчими станціями (мемоізація), холодильником (кеш даних), готовими стравами на роздачі (кеш маршруту) і нотатником офіціанта (кеш маршрутизатора) - Серверні кеші переживають деплойменти; кеш маршрутизатора зникає з закриттям вкладки - Правило вибору: `cache: 'no-store'` для живих даних, статичний режим за замовчуванням для блогів і каталогів - Головна пастка: `fetch` з `no-store` НЕ обходить кеш повного маршруту без `dynamic = 'force-dynamic'` ### Швидкий приклад ```tsx // 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` і кастомні заголовки для вибору варіанту на кожен запит ### Як працює ревалідація Два підходи. Часовий задає вікно застарілості: ```tsx const res = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } // застаріє через 5 хвилин }) ``` Подієвий через теги дозволяє скидати кеш на вимогу: ```tsx // При отриманні даних 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`** ```tsx // Неправильно: 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 до мутації. Виправлення: ```tsx 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 ```tsx // 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-сторінка товару з тегованою ревалідацією ```tsx // 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-проектів. ### Просунутий: динамічний маршрут з частковим кешуванням ```tsx // 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`. Динамічний рендеринг і кешування даних - це два незалежні перемикачі. Комбінування обох дозволяє отримувати свіжі дані користувача поряд із закешованими спільними даними в одному рендері.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.