Кеширане в Next.js 15: Пълно ръководство за use cache, cacheTag, cacheLife и ревалидация

Пълно ръководство за кеширане в Next.js 15 с практически примери. Научете как да използвате use cache, cacheTag, cacheLife, revalidatePath и revalidateTag за оптимална производителност на вашите приложения.

Защо кеширането в Next.js 15 е съвсем различно

Ако сте работили с Next.js 14, знаете усещането — извиквате fetch в сървърен компонент и данните магически се кешират. Звучи удобно, нали? Само че на практика това „магическо" поведение водеше до доста объркващи бъгове. Потребителите виждаха остарели данни, разработчиците не разбираха защо промените не се отразяват, а дебъгването на кеша... е, то се превърна в самостоятелна дисциплина.

Next.js 15 промени всичко с една решителна стъпка: кешът е изключен по подразбиране. Fetch заявките, GET Route Handlers и клиентската навигация вече не кешират нищо, освен ако изрично не го поискате. Философията е проста — вие решавате какво да се кешира, кога и за колко дълго.

Честно казано, след като работих по няколко проекта с новия модел, мога да кажа — промяната е към по-добро. Да, изисква повече съзнателни решения от страна на разработчика, но пък елиминира цяла категория трудно проследими бъгове.

В това ръководство ще разгледаме всяка част от новия кеширащ модел — от трите кеш слоя, през директивата 'use cache' и конфигурацията на dynamicIO, до практическо използване на cacheTag, cacheLife, revalidatePath и revalidateTag. Всичко с работещи примери, които можете директно да приложите.

Трите кеш слоя, които трябва да познавате

Next.js 15 работи с три отделни кеш механизма. Разбирането на всеки от тях е ключово, защото те взаимодействат помежду си — и грешна конфигурация на един може спокойно да обезсмисли другите.

Data Cache (сървърен)

Data Cache съхранява резултатите от fetch() извиквания и кеширани функции на сървъра. Тези данни преживяват между заявки и дори между деплойменти. В Next.js 15 обаче този кеш е празен по подразбиране — трябва изрично да го активирате за всяко извикване.

// Без кеш (по подразбиране в Next.js 15)
const data = await fetch('https://api.example.com/products')

// С принудително кеширане
const data = await fetch('https://api.example.com/products', {
  cache: 'force-cache'
})

// С времева ревалидация (ISR)
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 } // ревалидация на всеки час
})

Full Route Cache (сървърен)

Когато маршрут е статично рендериран, Next.js запазва готовия HTML и RSC payload в Full Route Cache. При следваща заявка сървърът директно връща предварително генерирания отговор — без повторно рендериране.

Важно е да знаете: ревалидирането на Data Cache автоматично инвалидира Full Route Cache, защото рендерът зависи от данните. Обратното обаче не е вярно.

Router Cache (клиентски)

Router Cache живее в браузъра и ускорява навигацията между страниците. В Next.js 15 страничните сегменти не се кешират по подразбиране от клиентския кеш — всяка навигация зарежда свежи данни. Ако искате да възстановите старото поведение (не че го препоръчвам за повечето случаи), можете да конфигурирате staleTimes:

// next.config.ts
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,  // 30 секунди за динамични страници
      static: 180,  // 3 минути за статични страници
    },
  },
}
export default nextConfig

Важен нюанс: когато извикате revalidateTag или revalidatePath от Server Action, целият клиентски кеш се изчиства незабавно. Това гарантира, че потребителят вижда свежи данни веднага след мутация.

Директивата use cache и dynamicIO

Директивата 'use cache' е може би най-значимата нова функционалност за кеширане в Next.js 15. Тя ви позволява да кеширате конкретни функции, компоненти или цели файлове, без да се налага да обгръщате всичко в fetch извиквания. Работи с бази данни, файлова система, изчисления — каквото и да се изпълнява на сървъра.

И ето къде нещата стават наистина интересни.

Активиране на dynamicIO

За да използвате 'use cache', първо трябва да активирате dynamicIO в конфигурацията. Когато dynamicIO е включен, Next.js спира автоматичното pre-rendering за операции за извличане на данни, освен ако изрично не ги кеширате:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    dynamicIO: true,
  },
}
export default nextConfig

Кеширане на ниво функция

Най-гъвкавият подход е да кеширате отделни функции. Аргументите и затворените стойности от родителския скоуп автоматично стават част от кеш ключа:

import { cacheTag, cacheLife } from 'next/cache'

export async function getProductById(id: string) {
  'use cache'
  cacheTag(`product-${id}`)
  cacheLife('hours')

  const product = await db.product.findUnique({
    where: { id },
    include: { category: true, reviews: true }
  })

  return product
}

Тук всяко различно id създава отделен кеш запис. Когато продукт бъде обновен, можете да инвалидирате само неговия кеш, без да засягате другите. Елегантно, нали?

Кеширане на ниво компонент

Можете да кеширате и цели компоненти. Това е идеално за сайдбари, навигации и други елементи, които се променят рядко:

export async function Sidebar() {
  'use cache'
  cacheLife('days')
  cacheTag('sidebar')

  const categories = await db.category.findMany({
    orderBy: { sortOrder: 'asc' }
  })

  return (
    <nav>
      {categories.map(cat => (
        <a key={cat.id} href={`/category/${cat.slug}`}>
          {cat.name}
        </a>
      ))}
    </nav>
  )
}

Кеширане на ниво файл

Когато поставите 'use cache' в началото на файл, всички експортирани функции и компоненти от него се кешират автоматично:

// app/dashboard/analytics/page.tsx
'use cache'

import { cacheLife } from 'next/cache'

cacheLife('minutes')

export default async function AnalyticsPage() {
  const stats = await getAnalytics()
  return <Dashboard data={stats} />
}

cacheLife: Контрол над времето на живот на кеша

cacheLife задава колко дълго кешираните данни остават валидни. Next.js предлага вградени профили, а вие можете да си дефинирате и собствени — нещо, което аз лично ползвам в почти всеки проект.

Вградени профили

Всеки профил контролира три параметъра: stale (колко дълго клиентът сервира стар кеш), revalidate (на колко време сървърът проверява за нови данни) и expire (максимален живот на кеш записа).

// Вградени профили:
cacheLife('seconds')  // stale: нула, revalidate: 1s, expire: 60s
cacheLife('minutes')  // stale: 5min, revalidate: 1min, expire: 1h
cacheLife('hours')    // stale: 5min, revalidate: 1h, expire: 24h
cacheLife('days')     // stale: 5min, revalidate: 1d, expire: 7d
cacheLife('weeks')    // stale: 5min, revalidate: 7d, expire: 30d
cacheLife('max')      // stale: 5min, revalidate: 30d, expire: никога

Дефиниране на персонализирани профили

За по-фин контрол можете да създадете собствени профили в next.config.ts. Това е особено полезно когато различни типове данни изискват различна „свежест":

// next.config.ts
const nextConfig = {
  experimental: {
    dynamicIO: true,
  },
  cacheLife: {
    productListing: {
      stale: 60,       // клиентът сервира стар кеш 1 минута
      revalidate: 300,  // сървърът обновява на 5 минути
      expire: 3600,     // максимален живот — 1 час
    },
    blogPost: {
      stale: 300,        // стар кеш до 5 минути
      revalidate: 86400, // обновяване веднъж на ден
      expire: 604800,    // максимален живот — 1 седмица
    },
    livePrice: {
      stale: 0,         // никога не сервира стар кеш
      revalidate: 5,    // обновяване на всеки 5 секунди
      expire: 60,       // изтича след 1 минута
    },
  },
}
export default nextConfig

След това ги използвате навсякъде в приложението:

import { cacheLife, cacheTag } from 'next/cache'

export async function getProductList(categorySlug: string) {
  'use cache'
  cacheTag(`products-${categorySlug}`)
  cacheLife('productListing')

  return await db.product.findMany({
    where: { category: { slug: categorySlug } },
    orderBy: { createdAt: 'desc' },
  })
}

cacheTag и ревалидация по таг

cacheTag маркира кеш записи с произволни етикети, които после можете да инвалидирате целенасочено. Това е много по-прецизно от revalidatePath, защото един таг може да обхваща данни, използвани на множество различни страници.

С две думи — ако revalidatePath е чук, то cacheTag + revalidateTag е скалпел.

Практически пример: блог система

// lib/data/posts.ts
import { cacheTag, cacheLife } from 'next/cache'

export async function getPostBySlug(slug: string) {
  'use cache'
  cacheTag(`post-${slug}`, 'posts')
  cacheLife('days')

  return await db.post.findUnique({
    where: { slug },
    include: { author: true, tags: true }
  })
}

export async function getRecentPosts(limit: number = 10) {
  'use cache'
  cacheTag('posts', 'recent-posts')
  cacheLife('hours')

  return await db.post.findMany({
    take: limit,
    orderBy: { publishedAt: 'desc' },
    include: { author: true }
  })
}

Забележете, че getPostBySlug получава два тага: специфичен за поста (post-my-slug) и общ за всички постове (posts). Идеята е следната — когато обновите един пост, инвалидирате само неговия таг. А когато добавите нов пост, инвалидирате общия таг posts, което засяга и списъка с последни постове. Просто и ефективно.

Инвалидация от Server Action

// app/actions/posts.ts
'use server'

import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function updatePost(formData: FormData) {
  const slug = formData.get('slug') as string
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  await db.post.update({
    where: { slug },
    data: { title, content, updatedAt: new Date() }
  })

  // Инвалидираме само кеша на конкретния пост
  revalidateTag(`post-${slug}`)
  redirect(`/blog/${slug}`)
}

export async function createPost(formData: FormData) {
  const data = Object.fromEntries(formData)

  await db.post.create({ data })

  // Инвалидираме общия таг, за да се обновят списъците
  revalidateTag('posts')
  redirect('/blog')
}

revalidatePath vs revalidateTag: Кога кое да ползвате

Тези две функции решават различни проблеми и е важно да знаете кога коя да изберете. Объркването между тях е една от най-честите грешки, които виждам в реални проекти.

revalidatePath

revalidatePath инвалидира кеша за конкретен URL път. Полезно е когато знаете точно кой маршрут трябва да се обнови:

import { revalidatePath } from 'next/cache'

// Инвалидиране на конкретна страница
revalidatePath('/blog/my-post')

// Инвалидиране на динамичен маршрут с всички варианти
revalidatePath('/blog/[slug]', 'page')

// Инвалидиране на layout и всички дъщерни страници
revalidatePath('/dashboard', 'layout')

revalidateTag

revalidateTag инвалидира всички кеш записи, маркирани с даден таг, независимо на кои страници се използват:

import { revalidateTag } from 'next/cache'

// Инвалидиране на всички продукти — засяга всяка страница,
// която показва продукти
revalidateTag('products')

// Инвалидиране на конкретен продукт
revalidateTag('product-123')

Кога кое да изберете

Ето кратко обобщение:

  • revalidatePath — ползвайте когато знаете точния URL. Обхваща всички кеширани данни на дадения маршрут. Работи в Server Actions и Route Handlers.
  • revalidateTag — ползвайте когато данните се споделят между множество страници. Таргетира само записи с конкретния таг. Работи в Server Actions и Route Handlers. Препоръчително е с profile="max" за stale-while-revalidate.
  • updateTag (ново) — работи само в Server Actions. Инвалидира кеша незабавно, без stale фаза. Идеално за сценарии, в които потребителят трябва веднага да види промяната си (т.нар. read-your-own-writes).

По мое наблюдение, в повечето реални приложения revalidateTag се оказва по-полезен, защото данните рядко живеят само на една страница.

ISR в App Router: Времева ревалидация на практика

Incremental Static Regeneration (ISR) остава мощен инструмент за страници, които се обновяват периодично. В App Router конфигурирането е по-просто от всякога — буквално един ред код.

Конфигуриране на ниво маршрут

// app/blog/[slug]/page.tsx
export const revalidate = 3600 // ревалидация на всеки час

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await db.post.findUnique({ where: { slug } })

  if (!post) notFound()

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Конфигуриране на ниво fetch

Можете да зададете различно време за ревалидация на отделни fetch заявки в рамките на един маршрут. Това дава доста гъвкавост:

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  // Продуктовите данни — ревалидация на всеки час
  const product = await fetch(`https://api.store.com/products/${id}`, {
    next: { revalidate: 3600 }
  })

  // Ревютата — ревалидация на всеки 5 минути
  const reviews = await fetch(`https://api.store.com/products/${id}/reviews`, {
    next: { revalidate: 300 }
  })

  // ...render
}

Когато в един маршрут има множество fetch заявки с различни интервали, най-краткият интервал определя ревалидацията на целия маршрут. Имайте го предвид при планиране — това е детайл, който лесно се забравя.

On-Demand ревалидация чрез Webhooks

За съдържание от CMS системи (Sanity, Contentful, Strapi и подобни), on-demand ревалидацията е най-ефективният подход. Вместо да чакате таймер, инвалидирате кеша в момента, в който съдържанието бъде обновено.

Route Handler за webhook

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  // Проверка на секретния токен
  const secret = request.headers.get('x-revalidation-secret')
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Невалиден токен' }, { status: 401 })
  }

  const body = await request.json()
  const { tag } = body

  if (!tag || typeof tag !== 'string') {
    return NextResponse.json({ error: 'Липсва таг' }, { status: 400 })
  }

  revalidateTag(tag)

  return NextResponse.json({
    revalidated: true,
    tag,
    timestamp: Date.now()
  })
}

В CMS системата конфигурирайте webhook, който изпраща POST заявка към /api/revalidate с хедър x-revalidation-secret и тяло { "tag": "posts" }. При всяка публикация кешът ще се инвалидира незабавно. Просто и ефикасно.

Типични грешки и как да ги избегнете

След работа с множество проекти, мигриращи от Next.js 14 към 15, ето кои грешки се повтарят най-често. Ако можете да избегнете тези пет капана, ще си спестите часове дебъгване.

1. Забравяте Router Cache

Ревалидирали сте данните на сървъра, но потребителят все още вижда старото? Проблемът почти сигурно е Router Cache в браузъра. Когато ревалидирате от Server Action, клиентският кеш се изчиства автоматично. Но ако ревалидирате от Route Handler (например чрез webhook), трябва да накарате клиента да зареди свежи данни чрез router.refresh().

2. Липсва dynamicIO при use cache

Директивата 'use cache' изисква dynamicIO: true в конфигурацията. Без нея ще получите грешка при билд. Винаги проверявайте next.config.ts преди да започнете да кеширате — повярвайте, на мен ми се е случвало да пропусна точно това.

3. Прекалено агресивно кеширане

Не кеширайте всичко. Потребителски данни, кошници за пазаруване и персонализирано съдържание не бива да се кешират споделено. Използвайте 'use cache' само за данни, които са еднакви за всички потребители, или се уверете, че потребителският идентификатор е част от кеш ключа.

4. Не тествате кеша в production режим

Това е класика. В режим на разработка (next dev) кеширането работи по различен начин. Винаги тествайте стратегията си с next build && next start, за да видите реалното поведение. Изненадите при деплой не са приятни.

5. Използване на остарелия unstable_cache

Ако все още ползвате unstable_cache, време е да мигрирате към 'use cache' с cacheTag и cacheLife. Новият подход е по-четим, по-безопасен по тип и официално поддържан.

Пълен пример: E-commerce каталог с кеширане

Хайде да съберем всичко в реалистичен сценарий — каталог на онлайн магазин с продукти, категории и ревюта. Този пример е доста близък до нещо, което бихте имали в реален проект.

// lib/data/products.ts
import { cacheTag, cacheLife } from 'next/cache'
import { db } from '@/lib/db'

export async function getFeaturedProducts() {
  'use cache'
  cacheTag('featured-products', 'products')
  cacheLife('hours')

  return await db.product.findMany({
    where: { isFeatured: true, isActive: true },
    include: { category: true },
    orderBy: { sortOrder: 'asc' },
    take: 12,
  })
}

export async function getProductBySlug(slug: string) {
  'use cache'
  cacheTag(`product-${slug}`, 'products')
  cacheLife('days')

  return await db.product.findUnique({
    where: { slug },
    include: {
      category: true,
      reviews: { orderBy: { createdAt: 'desc' }, take: 20 },
    },
  })
}

export async function getCategoryProducts(
  categorySlug: string,
  page: number = 1
) {
  'use cache'
  cacheTag(`category-${categorySlug}`, 'products')
  cacheLife('hours')

  const pageSize = 24
  return await db.product.findMany({
    where: { category: { slug: categorySlug }, isActive: true },
    skip: (page - 1) * pageSize,
    take: pageSize,
    orderBy: { createdAt: 'desc' },
  })
}
// app/actions/products.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function updateProduct(formData: FormData) {
  const id = formData.get('id') as string
  const slug = formData.get('slug') as string

  await db.product.update({
    where: { id },
    data: {
      name: formData.get('name') as string,
      price: Number(formData.get('price')),
      description: formData.get('description') as string,
    },
  })

  // Инвалидираме конкретния продукт и featured списъка
  revalidateTag(`product-${slug}`)
  revalidateTag('featured-products')
}

export async function toggleFeatured(id: string, slug: string) {
  const product = await db.product.findUnique({ where: { id } })
  await db.product.update({
    where: { id },
    data: { isFeatured: !product?.isFeatured },
  })

  revalidateTag(`product-${slug}`)
  revalidateTag('featured-products')
}

Тази архитектура осигурява бързо зареждане (кешираните страници се сервират за милисекунди), прецизна инвалидация (обновяването на един продукт не засяга целия каталог) и лесна поддръжка — всяка функция за данни ясно декларира своята кеш стратегия.

Често задавани въпроси

Как мога да мигрирам от Next.js 14 кеширане към Next.js 15?

Първата стъпка е да осъзнаете, че fetch вече не кешира по подразбиране. Прегледайте всяко fetch извикване в проекта и добавете cache: 'force-cache' или next: { revalidate: N } там, където искате кеширане. За функции, които не използват fetch (например директни DB заявки), мигрирайте от unstable_cache към 'use cache' с cacheTag и cacheLife. И задължително тествайте с next build && next start преди деплой.

Каква е разликата между revalidateTag и updateTag?

revalidateTag работи в Server Actions и Route Handlers и поддържа stale-while-revalidate — потребителят получава стария кеш, докато свежите данни се генерират на заден план. updateTag работи само в Server Actions и инвалидира кеша незабавно, без stale фаза. Ползвайте updateTag когато потребителят трябва веднага да види промяната си.

Мога ли да кеширам персонализирано съдържание за отделни потребители?

Да, стига потребителският идентификатор да е част от кеш ключа. При 'use cache' аргументите на функцията автоматично стават част от ключа — така getUserDashboard(userId) създава отделен кеш запис за всеки потребител. Бъдете внимателни обаче: при голям брой потребители това може да доведе до огромен кеш. Затова задайте кратък cacheLife.

Защо промените ми не се отразяват след ревалидация?

Най-честата причина е Router Cache в браузъра. Ако ревалидирате от Server Action, клиентският кеш се изчиства автоматично — проблемът не би трябвало да е там. Но ако ревалидирате от Route Handler, клиентът не знае за промяната. Решението е да използвате router.refresh() на клиента или просто да задействате ревалидацията от Server Action вместо от Route Handler.

Трябва ли dynamicIO за всеки проект, който иска да кешира?

Не задължително. dynamicIO е необходим конкретно за директивата 'use cache'. Ако разчитате само на fetch с cache: 'force-cache' или next: { revalidate }, и на маршрутните експорти (export const revalidate = 60), dynamicIO не е задължителен. Препоръчвам го обаче за нови проекти, защото 'use cache' предлага значително по-гъвкав контрол.

За Автора Editorial Team

Our team of expert writers and editors.