Next.js 16.4 додав три API для контролю того, коли і де виконується твій код. ensureStatic – build-time гарантія, що маршрут (або всі маршрути layout-а) лишається статичним: динамічний компонент у ньому валить build, а не тихо збільшує рахунок за сервер. navigation() виключає контент із prefetch і відкладає його до реальної навігації. prefetch() робить те саме для shell маршруту. Усі три – частина моделі Cache Components, яка з 16.4 рекомендована для всіх застосунків.

Проблема: оптимізація, яку ніхто не просив

Next.js дуже любить оптимізувати навігацію. Проблема починається тоді, коли «оптимізація» означає, що браузер або сервер робить роботу, яку користувач ніколи не попросить.

Два класичні сценарії:

  1. Випадкова динамічність. Сторінка блогу була статичною, поки хтось не додав у layout компонент із getCurrentUser(). Build пройшов, деплой пройшов, а сторінка тихо стала request-time – з усіма наслідками для вартості й latency. Ніхто не помітив, бо нічого не зламалось. Зламався лише профіль витрат.

  2. Жадібний prefetch. Список із 50 посилань, кожне з prefetch – і браузер завчасно тягне 50 повних сторінок разом із даними. UX чудовий, рахунок за compute і база даних під навантаженням – менш чудові.

16.4 дає інструменти проти обох.

ensureStatic: build-time guardrail

Ідея проста: якщо маршрут МАЄ бути статичним, зроби це контрактом, який перевіряє build.

export const ensureStatic = 'navigation';

export default function Page() {
  return (
    <>
      <UserAvatar /> {/* 🔴 цей компонент завалить build */}
      <Content />
    </>
  );
}

UserAvatar читає поточного користувача на request-time – і з ensureStatic = 'navigation' build чесно впаде замість того, щоб мовчки змінити характеристики сторінки.

Рівні три, від найсуворішого до точкового:

  • 'navigation' – навігації на цей маршрут ніколи не рендеряться на request-time. Жодного динамічного контенту взагалі.
  • 'prefetch' – лінки з явним prefetch на цей маршрут тягнуть лише статичний контент.
  • 'shell' – статичним гарантовано лише shell (те, що вантажиться при першому виявленні маршруту).

І найприємніше: ensureStatic працює не лише на сторінці, а й на layout. Один рядок у root layout:

// app/layout.tsx
export const ensureStatic = 'navigation';

і кожна сторінка сайту гарантовано статична. Коли якійсь сторінці знадобиться динаміка – спускаєш гарантію на вкладені layout-и, лишаючи суворою решту. Для лендингів, блогів, документації і ecommerce-категорій це саме той guardrail, якого бракувало: архітектурне рішення «цей сайт статичний» тепер захищене механізмом, а не дисципліною команди.

Офіційний приклад із реліз-поста – поштовий застосунок. Inbox рендерить лінки на листи, сторінка листа показує сам лист і тред. Додаєш prefetch на лінки – і листи відкриваються миттєво, без спінера. Але тепер кожен видимий лінк тягне весь тред заздалегідь – навіть для листів, які користувач ніколи не відкриє.

Рішення 16.4 – винести важку частину в компонент і позначити її navigation():

import { navigation } from 'next/cache';

async function Message({ id }) {
  const message = await getMessage(id);

  return (
    <>
      <p>{message.subject}</p>
      <div>{message.body}</div>

      <Suspense fallback={<Spinner />}>
        <Thread id={id} />
      </Suspense>
    </>
  );
}

async function Thread({ id }) {
  await navigation();
  const thread = await getThread(id);
  // ...
}

Тепер prefetch тягне лише перший лист, а тред вантажиться тоді, коли користувач реально перейшов. await navigation() читається буквально: «цей код чекає справжньої навігації».

Поруч із ним – prefetch(): він виключає кешований контент зі shell маршруту й відкладає його до явного prefetch через <Link prefetch> або useRouter().prefetch().

Це маленькі API з великою ідеєю: prefetch-бюджет має бути явним. Frontend performance давно вже не тільки про bundle size. У server-centric фреймворку prefetch – це compute, DB reads, cache hit rate і speculative робота, за яку хтось платить. Prefetch може одночасно покращити UX і збільшити backend-витрати – і тепер цей компроміс можна виражати в коді, а не в постфактум-розборі рахунку. Як це пов'язано з метриками користувача – ми детально розбирали в статті про Core Web Vitals для React-розробника.

Де нові API боляче кусаються

  • ensureStatic = 'navigation' на сторінці з cookies/headers. Build упаде – і це правильно. Помилка не в API, а в тому, що сторінка вдавала статичну. Спочатку винеси динаміку в Suspense-межу з усвідомленим рішенням, потім додавай гарантію.
  • navigation() як «вимикач повільного». Якщо контент повільний і при реальній навігації, виключення його з prefetch лише ховає проблему за спінером. Спочатку поклади його в 'use cache', потім вирішуй, чи потрібен він у prefetch.
  • Гарантія на root layout у динамічному застосунку. Якщо половина сторінок персоналізована, ensureStatic на root перетворить розробку на боротьбу з build-ом. Це інструмент для сайтів, які СВІДОМО статичні.
  • Плутати navigation() і prefetch(). Перший відкладає до навігації, другий – до явного prefetch. Якщо сумніваєшся, яка стадія тобі потрібна, подивись розбір Stream/Cache/Block у 16.3 – модель стадій там.

Знайди свою «майже статичну» сторінку

Візьми свій Next.js-проєкт і знайди одну сторінку, яка «мала би бути статичною»: блог, маркетинг, документація. Додай їй export const ensureStatic = 'navigation' і запусти build. Якщо він упав – вітаю, ти щойно знайшов випадкову динамічність, про яку не знав. Розберися, звідки вона прийшла (частіше за все – shared layout або «тимчасовий» компонент), і вирішуй усвідомлено: прибрати динаміку чи послабити гарантію до 'shell'.

Другий крок – знайди список лінків із prefetch і подивись у network/server-логи, скільки даних тягнеться заздалегідь. Якщо там повні сторінки з важкими запитами – ти знайшов кандидата на navigation().

Next.js 16.4 продовжує курс, який нам подобається: менше «фреймворк вирішує за тебе», більше «розробник декларує намір – фреймворк гарантує виконання».