Next.js 16.4 додав три API для контролю того, коли і де виконується твій код. ensureStatic – build-time гарантія, що маршрут (або всі маршрути layout-а) лишається статичним: динамічний компонент у ньому валить build, а не тихо збільшує рахунок за сервер. navigation() виключає контент із prefetch і відкладає його до реальної навігації. prefetch() робить те саме для shell маршруту. Усі три – частина моделі Cache Components, яка з 16.4 рекомендована для всіх застосунків.
Проблема: оптимізація, яку ніхто не просив
Next.js дуже любить оптимізувати навігацію. Проблема починається тоді, коли «оптимізація» означає, що браузер або сервер робить роботу, яку користувач ніколи не попросить.
Два класичні сценарії:
Випадкова динамічність. Сторінка блогу була статичною, поки хтось не додав у layout компонент із
getCurrentUser(). Build пройшов, деплой пройшов, а сторінка тихо стала request-time – з усіма наслідками для вартості й latency. Ніхто не помітив, бо нічого не зламалось. Зламався лише профіль витрат.Жадібний 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, якого бракувало: архітектурне рішення «цей сайт статичний» тепер захищене механізмом, а не дисципліною команди.
navigation(): prefetch-бюджет стає явним
Офіційний приклад із реліз-поста – поштовий застосунок. 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 продовжує курс, який нам подобається: менше «фреймворк вирішує за тебе», більше «розробник декларує намір – фреймворк гарантує виконання».