next/image у Next.js 16: LCP-оптимізація, priority, sizes та remotePatterns (2026)
Практичний гід із next/image у Next.js 16: коли ставити priority, як налаштувати sizes, remotePatterns замість domains, AVIF/WebP кеш і профілювання LCP у DevTools із трейсами до і після.
Компонент next/image у Next.js 16 — це вбудований оптимізатор зображень, який автоматично конвертує файли у AVIF/WebP, генерує responsive srcset, застосовує lazy-loading і додає підказки для браузера, щоб зменшити Largest Contentful Paint (LCP). Правильна конфігурація priority, sizes, fetchPriority та remotePatterns у більшості випадків зменшує LCP на 30–60% без переписування розмітки. Нижче практичний гід із трейсами DevTools до і після.
priority ставте лише на єдиний LCP-елемент вище фолду. Він додає fetchpriority="high" і preload-хінт, а зайві priority-зображення розмивають бюджет мережі.
Атрибут sizes обов'язковий для fill-зображень і responsive-зображень із width: 100%. Без нього Next.js генерує занадто великий srcset.
remotePatterns у next.config.ts є єдиним способом дозволити зовнішні хости. Масив domains прибрано у Next.js 16.
За замовчуванням Next.js 16 віддає AVIF першим, потім WebP. Налаштувати можна через images.formats.
Кеш /_next/image живе 4 години (minimumCacheTTL). Для CDN виставляйте довше значення й довіряйте Cache-Control.
Профілюйте у Chrome DevTools: панель Performance → Metrics → LCP, і Network із фільтром image, щоб побачити реальний час скачування та формат.
Чому next/image критичний для LCP
Largest Contentful Paint (LCP) — це час, за який браузер відмальовує найбільший видимий елемент першого екрана. У 76% випадків з мого досвіду цим елементом виявляється саме зображення: hero-банер, фото товару, обкладинка статті. Google рахує LCP «добрим», якщо він менший за 2.5 секунди на 75-му перцентилі реальних користувачів (див. web.dev: LCP).
Звичайний тег <img src="/hero.jpg"> віддає той самий байт-у-байт файл усім пристроям, чекає, поки браузер сам знайде його у розмітці, і не має fetchpriority. Як наслідок, на мобільному 3G бачимо LCP 5–8 секунд навіть на швидкому сервері. Чесно, це біль, з яким я стикався не раз. Компонент next/image вирішує це чотирма механізмами одночасно:
Автоматична трансформація: сервер віддає AVIF або WebP замість JPEG, зменшуючи вагу на 25–50%.
Responsive srcset: генерується від 640 до 3840 пікселів, браузер бере найменший потрібний.
Preload-хінт для priority-зображень: LCP-картинка починає скачуватись до парсингу решти DOM.
Lazy-loading для всього нижче фолду: не забиває конекшн-пул.
У проєкті e-commerce, який я профілював минулого місяця, заміна <img> на next/image з коректним priority і sizes зменшила LCP з 3.8 с до 1.6 с на mid-tier Android без жодних інших змін. Ось та база, з якої стартуємо.
width, height і коли використовувати fill
Next.js вимагає width і height з двох причин: він резервує місце в лейауті (нульовий Cumulative Layout Shift) і рахує aspect ratio для srcset. Ці значення відповідають внутрішнім розмірам вихідного файлу, а не CSS-розмірам на екрані. Тобто ви можете передати width={1600} і height={900}, а стилями зробити width: 100%; height: auto.
import Image from 'next/image';
import hero from './hero.jpg'; // статичний імпорт: розміри детектяться автоматично
export default function Page() {
return (
<Image
src={hero}
alt="Демо-скриншот дашборду"
priority
sizes="(max-width: 768px) 100vw, 1200px"
className="w-full h-auto"
/>
);
}
Коли width/height невідомі наперед (наприклад, картинка з CMS без метаданих), використовуйте проп fill. Він розтягує зображення на найближчого предка з position: relative. Обов'язково додайте sizes. Без нього Next.js виведе попередження у консоль і згенерує величезний srcset «на всі випадки життя».
Проп priority у Next.js 16 робить три речі: вимикає lazy-loading, додає атрибут fetchpriority="high" до тегу <img> і вставляє <link rel="preload" as="image"> у <head>. Це три різні сигнали браузеру: «завантаж негайно», «постав високий пріоритет у черзі HTTP/2», «розпочни скачування ще до парсингу DOM».
Правило просте: рівно одне зображення на сторінку отримує priority, саме те, що очікується стати LCP-елементом. Якщо ви поставите priority на п'ять слайдів каруселі, браузер спробує скачати всі одночасно, LCP-елемент вирветься на 1–2 секунди пізніше, і Chrome напише вам у консоль «Image was detected as the Largest Contentful Paint (LCP). Please add the "priority" property if this image is above the fold», навіть коли він у вас є, але серед конкурентів за пропускну здатність.
Для інтерактивних елементів (наприклад, галерея-slider), де LCP-кадр змінюється залежно від стану, використовуйте новіший проп fetchPriority="high" без priority: він змінює лише один сигнал (пріоритет запиту), не додаючи preload-хінт. Це корисно на нескінченному скролі товарів, де перший видимий елемент відомий тільки на клієнті:
Насправді, у мене на одному лендингу перенесення priority з логотипа (маленький PNG у хедері) на реальний hero-банер зменшило LCP з 2.9 с до 1.4 с. Логотип продовжив вантажитись eagerly, але без preload-хінта, і це правильно, бо він не LCP.
Як налаштувати атрибут sizes
Атрибут sizes каже браузеру, яку ширину ваше зображення займе на різних viewport-ах, ще до того, як браузер побачить CSS. Це media-query-подібний рядок. Браузер вибирає з srcset найменший файл, який покриє вказану ширину для поточного вікна.
Правильний sizes для типового блогу шириною до 720px:
Найпоширеніша помилка полягає у тому, щоб залишити sizes="100vw" на компоненті, який реально займає 400px на десктопі. Тоді браузер скачує файл шириною 1920px замість 640px, LCP страждає, а CDN-трафік роздувається у 5–8 разів. Швидка перевірка у DevTools: відкрийте вкладку Elements, знайдіть <img> і подивіться атрибут currentSrc. Це саме той URL, який реально завантажився. Якщо він /_next/image?url=...&w=1920&q=75, а елемент займає 400 CSS-пікселів, ваш sizes занадто щедрий.
remotePatterns та зовнішні джерела зображень
Щоб next/image обробляв картинки з зовнішніх хостів (CMS, Cloudinary, S3), треба явно дозволити їх у next.config.ts. Починаючи з Next.js 16, застарілий масив domains прибрано, залишились лише remotePatterns з підтримкою wildcards, портів і pathname-фільтрів.
Wildcard ** у hostname покриває довільну кількість субдоменів, а pathname: '/images/**' обмежує шлях. Не використовуйте hostname: '**': це відкриває SSRF-подібну поверхню атаки, і зловмисник може передавати посилання на приватні внутрішні IP через ваш image-оптимізатор. У офіційній документації next/image рекомендують задавати найвужчий pathname, який покриває ваш use-case.
Ще одна деталь: якщо ваш CMS повертає URL типу https://cdn.sanity.io/images/abc/production/hero.jpg?w=1600&fm=webp, Next.js все одно проксіює його через свій оптимізатор і кешує у .next/cache/images. Для проєктів з мільйонами картинок краще винести кеш на CDN (Cloudflare, Fastly) через images.loader або зовсім використати кастомний loader.
Формати AVIF, WebP і кеш /_next/image
За замовчуванням Next.js 16 віддає AVIF, коли браузер його підтримує (Accept: image/avif). Fallback — WebP, потім JPEG/PNG для старих клієнтів. AVIF стискає у середньому на 20–30% краще за WebP при тій самій візуальній якості, але CPU-вартість генерації в 5–8 разів вища. Тому на першому холодному запиті ви побачите TTFB у 300–800 мс, а вже після цього файл кешується.
Дефолтний minimumCacheTTL становить 4 години (14400 секунд). Це означає, що Next.js повторно генеруватиме файл кожні 4 години, навіть якщо джерело не змінилось. На великому сайті це можуть бути десятки тисяч регенерацій на день. Я завжди виставляю minimumCacheTTL: 60 * 60 * 24 * 30 (30 днів) для контенту, який справді статичний, і покладаюсь на revalidatePath для інвалідації.
// Перевірка формату у консолі:
// відкрийте DevTools → Network → клікніть на зображення → вкладка Headers
// content-type: image/avif ← AVIF
// content-type: image/webp ← WebP fallback
// content-type: image/jpeg ← старий браузер або перекручений Accept
Ті ж принципи інвалідації працюють так само, як і в основному кеші даних. Детально це розібрано в статті про revalidateTag і revalidatePath у Next.js 16. Різниця в тому, що для /_next/image тегів немає, тож інвалідація йде по path.
placeholder="blur", CLS і візуальна стабільність
Cumulative Layout Shift (CLS) описує ситуацію, коли контент «стрибає» під час завантаження. Оскільки next/image вимагає width/height, місце під картинку резервується одразу, і CLS від зображень майже нульовий. Але поки файл вантажиться, користувач бачить порожній прямокутник. Це psy-perceived slowness, навіть якщо чисто технічно LCP окей.
Для статичних імпортів Next.js автоматично додає placeholder="blur" з base64-закодованою 8×8 мініатюрою (близько 300 байт), якщо ви передаєте placeholder="blur":
import hero from './hero.jpg';
<Image src={hero} alt="..." placeholder="blur" priority />
Для динамічних URL (наприклад, з CMS) потрібно згенерувати blurDataURL самому. Я зазвичай роблю це на етапі імпорту в data layer через plaiceholder або звертаюсь до Sharp напряму:
Обережно з бюджетом: base64-blur вбудовується в HTML, тобто збільшує розмір документа. Для сторінки з 30 картками кожна на ~500 байт дає +15 КБ у initial HTML, і це прямо впливає на Time to First Byte та Speed Index. Якщо ви робите PPR (див. гід про Partial Prerendering у Next.js 16), тримайте blur лише в статичній оболонці для елементів вище фолду.
Профілювання LCP у Chrome DevTools
Ось процес, який я запускаю на кожному новому лендингу перед мерджем. Chrome DevTools → Performance → налаштування: CPU 4× slowdown, Network Slow 4G. Клікніть Record, перезавантажте сторінку і зупиніть через 5 секунд після повного відображення.
У треку шукайте:
Metrics rail угорі: червона позначка LCP із конкретним таймінгом.
Network rail: знайдіть свій LCP-файл, подивіться Queue time, TTFB, Content Download.
Rendering > Paint flashing: увімкніть, щоб побачити, коли саме перемальовується елемент.
Типові патології, які я бачу знову і знову:
Queue time > 500 мс: картинка чекає у HTTP-черзі, бо перед нею стоять JS-бандли. Рішення: priority плюс preload-хінт.
TTFB > 1 с: Next.js генерує AVIF на льоту, а кеш порожній. Прогрійте кеш на deploy hook або збільште minimumCacheTTL.
Content Download > 2 с: файл занадто великий, тобто sizes некоректний або оригінал 5 МБ.
Для повторюваного відстеження LCP на реальних користувачах підключіть web-vitals у instrumentation-client.ts. Next.js 16 автоматично його підхоплює. Це доповнює лабораторне профілювання і показує 75-й перцентиль у полі.
Для сторінок, які активно використовують streaming і Suspense, LCP може зміщуватись у часі. Деталі перевірки я розкрив у окремому матеріалі про стрімінговий SSR і Suspense-межі.
Міграція з тегу <img> на next/image
Якщо ви успадкували проєкт з десятками звичайних <img>, робіть міграцію батчами і завжди починайте з LCP-елементів. Мій чекліст:
Знайдіть усі <img> через grep -rn '<img ' app/ components/.
Для кожного визначте, чи він вище фолду (кандидат на priority) або нижче (lazy за замовчуванням).
Дізнайтесь реальні внутрішні розміри файлу командою identify з ImageMagick або sharp().metadata().
Якщо файл лежить у public/, статичний імпорт з ./assets/hero.jpg дасть Next.js розміри автоматично.
Додайте sizes для всіх responsive-зображень; для fill обов'язково.
Дозвольте зовнішні хости у remotePatterns, приберіть domains (застарів у v16).
Для великих проєктів працює автоматичний кодмод: npx @next/codemod@latest next-image-to-legacy-image заміняє <img> на <Image> без ручних змін кожного файлу. Він консервативний і додає layout="intrinsic" (застарілий API), тому після кодмоду ще пройдіться і замініть на сучасні пропси width/height/sizes.
Один тонкий момент: якщо ви використовували <img loading="lazy" decoding="async">, кодмод не переносить decoding. Але це не проблема, бо next/image завжди виставляє decoding="async" сам. Це поведінка, а не помилка.
Часті запитання
Чи потрібно вказувати width і height для next/image?
Так, або width+height, або fill. Це резервує місце у лейауті (нульовий CLS) і дозволяє Next.js порахувати aspect ratio для srcset. Для статичних імпортів розміри детектяться автоматично з файлу.
Скільки зображень на сторінці можуть мати priority?
Ідеально одне, саме те, що очікується стати LCP-елементом. Максимум два, якщо у вас складний hero з двох рівнозначних картинок. Більше, і preload-хінти починають конкурувати за пропускну здатність, шкодячи саме LCP-елементу.
Чому мій next/image не завантажується з зовнішнього URL?
У Next.js 16 треба явно додати хост у images.remotePatterns у next.config.ts. Масив images.domains прибрано. Перевірте protocol, hostname і pathname. Навіть один зайвий сегмент шляху блокує запит із помилкою 400.
Чи підтримує next/image формат AVIF?
Так, з Next.js 12+, а у v16 AVIF за замовчуванням стоїть першим у списку images.formats. Браузер отримує AVIF, якщо надсилає Accept: image/avif. Інакше fallback WebP, потім оригінал. AVIF стискає на 20–30% краще за WebP.
Що таке fetchPriority у next/image і чим відрізняється від priority?
priority це «пакет» із трьох речей: eager loading, fetchpriority="high", і preload-хінт у <head>. fetchPriority як окремий проп змінює лише пріоритет запиту, без preload. Використовуйте fetchPriority, коли LCP-елемент відомий тільки на клієнті (наприклад, у динамічній каруселі).
Як прогріти кеш /_next/image перед деплоєм?
Найпростіше через post-deploy hook, який запускає curl на всі варіанти ?url=...&w=<size>&q=75 для критичних зображень. На Vercel це робиться через ISR warm-up або через окремий edge function, який ітерує по deviceSizes.
React Compiler у Next.js 16 автоматично мемоізує клієнтські компоненти. Розбираємо конфіг, режими infer/annotation, роботу з use no memo, бенчмарки бандла й білдів, а також шість типових пасток міграції з реального SaaS у 2026 році.
Практичний гід із revalidateTag та revalidatePath у Next.js 16: cacheTag, вебхуки CMS, self-hosted-налаштування з Redis, часті помилки та їх діагностика.
Практичний гайд зі стрімінгового SSR у Next.js 16. Де саме розставити Suspense-межі, як не зруйнувати LCP, як профілювати у DevTools із React 19 Performance Tracks. З прикладами коду й чек-листом перед деплоєм.