Skip to content
kobiakov.dev
Назад до блогу

Продакшн-архітектура Next.js 15: App Router, PPR і масштабоване кешування

Практичний посібник зі структури Next.js 15 застосунку для продакшену: Server Components, Partial Prerendering, інвалідація кешу та патерни розгортання.

Vladyslav Kobiakov3 хв читання
Продакшн-архітектура Next.js 15: App Router, PPR і масштабоване кешування

Це технічна стаття для розробників. Якщо ви керуєте салоном, рестораном або ремісничим бізнесом, почніть краще звідси: скільки насправді коштує сайт і що справді потрібно вашому сайту.

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

Ментальна модель: де виконується код?

Перш ніж написати хоча б один рядок, дайте відповідь на це питання для кожного компонента:

| Питання | Відповідь → | | ----------------------------------- | --------------------------------------- | | Дані змінюються для кожного запиту? | Server Component із cache: 'no-store' | | Дані змінюються за розкладом? | Server Component із revalidate | | Дані ніколи не змінюються? | Повністю статичний Server Component | | Потрібна інтерактивність? | Client Component ('use client') |

За замовчуванням App Router використовує Server Components. Додавання 'use client' — це явний вибір, а не вихідна точка.

Структура каталогів для реального застосунку

app/
  [locale]/
    (public)/          ← layout із navbar/footer
      page.tsx          ← головна, статична
      blog/
        page.tsx        ← список, revalidate: 60
        [slug]/page.tsx ← детальна сторінка, static + ISR
      portfolio/
        page.tsx        ← статична
    (protected)/       ← layout із перевіркою auth
      dashboard/
        page.tsx        ← динамічна, без кешу
  api/
    contact/route.ts   ← POST, з rate limit
    og/route.ts        ← динамічне OG-зображення

Route groups (public) і (protected) використовують різні layouts, не впливаючи на URL. Це чистіше, ніж обгортати кожну захищену сторінку в перевірку auth.

Server Components: патерни отримання даних

Паралельне отримання через Promise.all

Не використовуйте послідовні await, коли запити незалежні:

// ❌ 600ms (два послідовні запити) const projects = await getProjects(); const testimonials = await getTestimonials(); // ✅ 300ms (паралельно) const [projects, testimonials] = await Promise.all([getProjects(), getTestimonials()]);
ts

Дедуплікація запитів через React cache

Коли кільком компонентам на одній сторінці потрібні ті самі дані, React.cache гарантує, що fetch виконається один раз за render:

import { cache } from 'react'; export const getUser = cache(async (id: string) => { return prisma.user.findUnique({ where: { id } }); });
ts

І <Header>, і <Dashboard> можуть викликати getUser(id), але запит до бази даних виконається лише один раз.

Partial Prerendering (PPR)

PPR — найсуттєвіша архітектурна зміна в Next.js 15. Вона дає змогу зробити один route частково статичним:

// next.config.ts export default { experimental: { ppr: 'incremental', }, }; // app/[locale]/page.tsx export const experimental_ppr = true; export default function HomePage() { return ( <> <HeroSection /> {/* статична оболонка — миттєво віддається з CDN */} <Suspense fallback={<ProjectsSkeleton />}> <FeaturedProjects /> {/* динамічна частина — стримиться після оболонки */} </Suspense> </> ); }
ts

HTML-оболонка з <HeroSection> кешується на edge. Динамічні частини надходять потоком усередині Suspense boundary. Користувач бачить контент менш ніж за 100 ms, а персоналізовані дані з’являються трохи пізніше.

Стратегія інвалідації кешу

У Next.js 15 є три рівні кешу. Важливо розуміти, який із них потрібно інвалідувати:

import { revalidatePath, revalidateTag } from 'next/cache'; // Інвалідувати конкретний URL, наприклад після публікації допису await revalidatePath('/blog/[slug]', 'page'); // Інвалідувати всі сторінки з тегом 'projects' — рекомендовано для даних await revalidateTag('projects'); // Позначити fetch тегом, щоб потім інвалідувати його за цим тегом const data = await fetch('/api/projects', { next: { tags: ['projects'], revalidate: 300 }, });
ts

Підхід із revalidateTag краще масштабується, ніж revalidatePath, оскільки ви позначаєте дані на рівні fetch, а не route. Один API-виклик інвалідує кожну сторінку, яка використала ці дані.

Middleware для i18n і auth

Middleware виконується на edge до рендерингу сторінки. Залишайте його мінімальним — він працює для кожного запиту:

// middleware.ts import { NextRequest, NextResponse } from 'next/server'; import createIntlMiddleware from 'next-intl/middleware'; import { routing } from '@/i18n/routing'; const intlMiddleware = createIntlMiddleware(routing); export function middleware(request: NextRequest) { // Перевірка auth для захищених routes const isProtected = request.nextUrl.pathname.startsWith('/dashboard'); if (isProtected) { const session = request.cookies.get('next-auth.session-token'); if (!session) { return NextResponse.redirect(new URL('/login', request.url)); } } return intlMiddleware(request); }
ts

Уникайте запитів до бази даних у middleware. Перевіряйте лише cookies або JWT claims — усе важче має бути в Server Component сторінки.

Чекліст розгортання в продакшн

Перед push у Vercel:

# 1. Перевірка типів npx tsc --noEmit # 2. Lint npm run lint # 3. Локальний build для виявлення помилок статичної генерації npm run build # 4. Перевірка розміру bundle ANALYZE=true npm run build
bash

Звертайте увагу на:

  • Server Components, які випадково імпортують пакети лише для клієнта — window, localStorage
  • Виклики cookies() або headers() поза динамічним контекстом — це вимикає статичну генерацію для всього route
  • Зображення без явних width/height, які спричиняють layout shift

Висновок

Сила App Router походить із розуміння межі між статичним і динамічним кодом та її свідомого розташування. За замовчуванням обирайте статичний рендеринг, додавайте Suspense boundaries для динамічних секцій, позначайте fetch-запити тегами для точкової інвалідації та не перевантажуйте middleware. Ці чотири рішення запобігають 90% проблем із продуктивністю в продакшені ще до їх появи.

Працюєте над чимось подібним?

Я створюю швидкі маркетингові сайти й системи онлайн-запису для малого бізнесу по всій Європі — як незалежний фахівець, віддалено. Відповідаю протягом 24 годин.

Зв’язатися
Share: