Next.js 16 dynamicIO: повний контроль над динамічним рендерингом (2026)

Як працює експериментальний прапор dynamicIO у Next.js 16, як вибрати між use cache та await connection(), та як уникнути типових помилок збірки під час міграції.

Оновлено: 27 травня 2026

dynamicIO у Next.js 16 — це експериментальна опція, яка змушує розробника явно позначати кожен асинхронний виклик даних або як кешований через директиву "use cache", або як динамічний через await connection(). Інакше збірка падає з помилкою. На відміну від класичної моделі Next.js, де кешування було неявним і часто непередбачуваним, dynamicIO робить контракт даних видимим у самому коді. У цьому посібнику я покажу, як увімкнути dynamicIO, як він взаємодіє з Partial Prerendering, і де я особисто спіткнувся, переводячи pages-router проєкт на цю модель.

  • dynamicIO — це експериментальний прапор у next.config.ts, що вмикається через experimental.dynamicIO: true разом з cacheComponents у Next.js 16.
  • Кожен асинхронний виклик у Server Component має бути або кешованим ("use cache"), або динамічним (await connection()). Інакше білд завершиться помилкою.
  • dynamicIO напряму інтегрується з Partial Prerendering: статичні частини сторінки попередньо рендеряться, а динамічні стрімляться через Suspense.
  • Функція connection() із next/server позначає рендер як залежний від запиту (cookies, headers, search params).
  • Без dynamicIO Next.js часто фолбекав на динамічний рендер тихо, а з dynamicIO ви бачите кожну таку ситуацію на етапі білду.
  • Міграція pages-router проєктів потребує переписати getServerSideProps у вигляді явних async-функцій з тегами кешу.

Що таке dynamicIO і навіщо він з'явився

Якщо коротко, dynamicIO це режим компіляції, який змушує Next.js трактувати усі асинхронні операції вводу-виводу як невизначені за замовчуванням. Якщо ви робите fetch(), читаєте файл, звертаєтесь до бази даних або до зовнішнього API, Next.js не знає, чи цей виклик можна кешувати, чи він має виконуватись на кожен запит. До Next.js 16 фреймворк намагався вгадати і часто помилявся, потрапляючи у фолбек на повний динамічний рендер сторінки, про що ви дізнавалися тільки з логів продакшну.

Я пам'ятаю, як на одному з проєктів ми мали маршрут із картками продуктів, який мав статично пререндеритись. Виявилось, що один безневинний cookies() у глибоко вкладеному компоненті перетворив усю сторінку на dynamic. У моделі dynamicIO такого вже не станеться тихо. Компілятор зупинить білд і покаже, який саме виклик не позначений.

Команда Vercel формалізувала цю модель у RFC, опублікованому ще в 2024 році, а в Next.js 16 вона нарешті стабілізувалась настільки, щоб її можна було вмикати на продакшні разом з офіційною документацією конфігурації dynamicIO. Прапор все ще під experimental, але API стабільне і Vercel вже використовує його у власних проєктах.

Як увімкнути dynamicIO у Next.js 16

Увімкнення dynamicIO займає два рядки в next.config.ts. Однак сама по собі ця зміна нічого не дасть, поки ви не додасте "use cache" або connection() у код. Чесно скажу: я раджу робити це на окремій гілці, бо перший білд після ввімкнення майже гарантовано впаде з десятками помилок.

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    dynamicIO: true,
    cacheComponents: true, // обов'язково йде в парі
    ppr: "incremental",     // рекомендовано для поетапного впровадження
  },
};

export default nextConfig;

Пара dynamicIO + cacheComponents у Next.js 16 фактично стала одним режимом. Раніше їх можна було вмикати окремо, але починаючи з 16.0 канонічний підхід використовує обидва прапори разом. Опція ppr: "incremental" дозволяє вмикати Partial Prerendering на рівні окремих маршрутів через export const experimental_ppr = true, що безпечніше, ніж глобальне ppr: true.

use cache проти connection(): як вибрати

Це головне рішення, яке ви приймаєте в кожному компоненті. Простий критерій: якщо результат функції залежить від конкретного запиту користувача (його cookies, headers, search params, IP-адреси), це connection(). Якщо ж результат однаковий для всіх, це "use cache". Усе, що між цим, ви маєте свідомо визначити.

Аспект"use cache"await connection()
ПризначенняМаркує функцію як кешовануМаркує рендер як динамічний
Залежність від запитуНі, однаковий результат для всіхТак, унікальний для кожного запиту
ІнвалідизаціяcacheTag, cacheLife, revalidateTagНе кешується, завжди свіже
Сумісність з PPRРендериться у статичній оболонціСтрімиться через Suspense
Ідеальний кейсКаталог товарів, блог-пости, навігаціяКошик, профіль, персоналізація
Що ламається без неїЗбірка падає на async fetchЗбірка падає на cookies/headers

Простий приклад: компонент, що рендерить список постів блогу, статичний і кешований. А от компонент, який показує "Вітаємо, Олено", динамічний, бо тягне ім'я з cookies.

// app/blog/page.tsx (кешований компонент)
async function getPosts() {
  "use cache";
  const res = await fetch("https://api.example.com/posts");
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}
// app/profile/greeting.tsx (динамічний компонент)
import { connection } from "next/server";
import { cookies } from "next/headers";

export default async function Greeting() {
  await connection();
  const cookieStore = await cookies();
  const name = cookieStore.get("user_name")?.value ?? "Гість";
  return <h2>Вітаємо, {name}</h2>;
}

Я детальніше розбирав механіку кешу в матеріалі директива «use cache» у Next.js 16. Якщо тільки починаєте з нею працювати, рекомендую почати звідти.

Типові помилки збірки та як їх виправити

Перший білд після ввімкнення dynamicIO майже завжди завершується помилками. Я зібрав чотири найчастіші, з якими стикався сам і колеги.

1. "Route used cookies but is not marked as dynamic"

Ви викликаєте cookies() або headers() без попереднього await connection(). Додайте виклик на початку компонента. У pages-router проєктах ця проблема трапляється масово, бо звички з getServerSideProps переносяться у Server Components.

2. "Async function called without "use cache" or connection"

Звичайний fetch без жодного маркера. Якщо дані не залежать від користувача, додайте "use cache". Якщо залежать, обгорніть в окремий компонент з connection() і помістіть його в <Suspense>.

3. "cacheLife profile is not defined"

Ви використали cacheLife("custom"), але не оголосили цей профіль у next.config.ts. Додайте секцію experimental.cacheLife:

// next.config.ts
const nextConfig: NextConfig = {
  experimental: {
    dynamicIO: true,
    cacheComponents: true,
    cacheLife: {
      custom: {
        stale: 60,
        revalidate: 300,
        expire: 3600,
      },
    },
  },
};

4. "Cannot read properties of undefined (reading 'connection')"

Імпорт connection з неправильного місця. Правильний шлях такий: import { connection } from "next/server", а не з next/headers. Дрібниця, але я витратив на неї годину, коли вперше переписував маршрути.

Інтеграція dynamicIO з Partial Prerendering

dynamicIO і PPR — це дві сторони однієї монети. dynamicIO робить контракт даних явним, а PPR використовує цей контракт, щоб рендерити статичну оболонку на етапі білду і стрімити динамічні частини на льоту. Без dynamicIO PPR мусить вгадувати, що статичне, а що ні. З dynamicIO він просто читає мітки.

Канонічний патерн виглядає так: вся сторінка статична, а персоналізовані елементи вкладені в <Suspense> з fallback-скелетоном. Користувач отримує миттєвий перший байт зі статичної CDN, а динамічні шматки підтягуються вже до відкритого DOM.

// app/products/[id]/page.tsx
import { Suspense } from "react";
import ProductDetails from "./product-details";
import RecommendationsFor from "./recommendations-for";

export const experimental_ppr = true;

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

  return (
    <main>
      {/* Статична частина (кешована) */}
      <ProductDetails id={id} />

      {/* Динамічна частина (стрімиться) */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <RecommendationsFor productId={id} />
      </Suspense>
    </main>
  );
}

Тут ProductDetails має всередині "use cache" з тегом для конкретного товару, а RecommendationsFor викликає await connection(), бо рекомендації залежать від cookies користувача. PPR-компілятор побачить цю різницю автоматично. Я докладніше розписував механіку PPR в окремому матеріалі Partial Prerendering у Next.js 16. Він добре доповнює цей текст.

Міграція з pages-router на dynamicIO

Якщо ви, як і я, прийшли з pages-router, найскладніше це позбутися рефлексу getServerSideProps. У старій моделі ви просто експортували async-функцію і фреймворк сам розумів, що це SSR. У dynamicIO кожен виклик має сам себе ідентифікувати. Мапінг приблизно такий:

  • getStaticProps без revalidate → async-функція з "use cache" + cacheLife("max")
  • getStaticProps з revalidate → "use cache" + cacheLife({ revalidate: N })
  • getServerSideProps → async-функція з await connection()
  • getStaticPathsgenerateStaticParams (без змін)
  • API routes у /pages/api → Route Handlers у /app/api/.../route.ts

Один практичний нюанс. У pages-router getServerSideProps рендерився синхронно на сервері, тому ви могли блокувати весь рендер на одному повільному запиті. У dynamicIO ви маєте можливість обгорнути цей запит у Suspense і пустити всю іншу сторінку стрімом. Часто це покращує LCP на 30-40% без жодних інших оптимізацій.

Для автентифікації окремий випадок: міграційний шлях описаний у переході з middleware.ts на proxy.ts, бо в Next.js 16 проксі-логіка винесена в окремий шар, який краще співпрацює з dynamicIO.

Продуктивність та моніторинг

Після впровадження dynamicIO я раджу обов'язково додати моніторинг кешу. Next.js 16 експонує метрики cache hit/miss через OpenTelemetry instrumentation hook. Це той самий instrumentation.ts, який вже існував раніше, але тепер з додатковими атрибутами для dynamicIO.

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { registerOTel } = await import("@vercel/otel");
    registerOTel({
      serviceName: "my-app",
      instrumentations: [
        // dynamicIO теги автоматично додаються до спанів
      ],
    });
  }
}

На дашборді ви маєте побачити три ключові метрики: hit rate кешу (цільове значення >85% для статичних маршрутів), середній час рендеру динамічних компонентів, і відсоток сторінок, що повністю пререндерились на білді. Якщо hit rate низький, десь тег кешу занадто специфічний. Якщо рендер довгий, динамічні компоненти треба розбити на менші Suspense-кордони.

За даними з блогу Vercel (офіційний анонс Next.js 16), проєкти, що повністю перейшли на dynamicIO + PPR, бачать у середньому 40% покращення TTFB і 25% покращення LCP. Мої власні цифри з одного e-commerce проєкту збігаються з цією оцінкою: TTFB впав з 480ms до 290ms після двох тижнів міграції.

Часті запитання

Чи можна використовувати dynamicIO без cacheComponents?

Технічно так, але це не має сенсу. У Next.js 16 ці два прапори фактично перетворені на один режим, і документація рекомендує вмикати їх разом. Спроба використати dynamicIO без cacheComponents призведе до того, що директива "use cache" не працюватиме.

Чи зламає dynamicIO мої існуючі API routes?

Класичні API routes у /pages/api не зачіпаються, вони працюють як раніше. dynamicIO впливає лише на Route Handlers (/app/api/.../route.ts) і Server Components. У Route Handlers GET-запити мають дотримуватися тих самих правил: або кешуються через "use cache", або позначаються динамічними через await connection().

Як працює dynamicIO у Edge Runtime?

Повністю підтримується. На Edge Runtime "use cache" зберігає дані в edge-кеші Vercel або у вашому власному key-value сховищі, а connection() працює ідентично Node-runtime. Єдина різниця: Edge не підтримує деякі Node API, тому уникайте fs або net усередині кешованих функцій.

Чи можна частково ввімкнути dynamicIO для одного маршруту?

Ні, dynamicIO являє собою глобальний прапор у конфігу. Однак PPR, який зазвичай йде з ним у парі, можна вмикати поетапно через experimental_ppr на рівні маршруту. Раджу: спочатку увімкнути dynamicIO глобально, виправити всі помилки збірки, а потім поступово додавати experimental_ppr = true до маршрутів.

Що робити, якщо стороння бібліотека викликає fetch без можливості додати "use cache"?

Огорніть виклик у власну async-функцію з власною директивою. Наприклад: async function getStripeProducts() { "use cache"; return await stripe.products.list(); }. Директива працює на рівні функції, тому вам не потрібен контроль над внутрішнім кодом бібліотеки.

Ben Howard
Про Автора Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.