Оптимізація зображень (next/image) у Next.js
next/image - компонент Next.js для оптимізації зображень, який автоматично конвертує їх у WebP або AVIF, генерує варіанти розмірів через srcset і завантажує їх з відкладенням (lazy loading) за замовчуванням.
Теорія
TL;DR
- Звичайний
<img>відправляє оригінальний файл (~2MB) на кожен пристрій;next/imageнарізає його на варіанти під конкретний екран і доставляє лише потрібний - Головний виграш: 35-50% менші файли на мобільних, бо браузер завантажує 400px WebP замість 2000px JPEG
- За замовчуванням: lazy loading, конвертація формату, запобігання CLS через зарезервовані розміри
priority={true}тільки для LCP зображень (hero-банери, контент вище згину); максимум 1-2 на сторінку- Зовнішні зображення потребують
remotePatternsуnext.config.js, інакше runtime помилка
Швидкий приклад
import Image from 'next/image'
// Звичайний img: завантажує оригінал 2000x1200px (~2MB) кожного разу
<img src="/hero.jpg" alt="Hero" width={2000} height={1200} />
// next/image: видає ~300KB WebP під реальний розмір пристрою, lazy за замовчуванням
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={2000}
height={1200}
priority // вище згину, тому завантажуємо заздалегідь
/>
)
}
// Мобільний отримає 400w WebP; десктоп - 1200w; браузер обирає через srcsetОдин момент з практики: якщо не вказати sizes для fill зображення, браузер вважає його 100vw на всіх брейкпоінтах і завантажує повноширокий варіант навіть для мініатюри в сайдбарі. Невелика помилка, але реальні витрати трафіку.
Головна різниця
Браузер не знає, якого розміру буде <img> у верстці, поки не завантажить весь CSS і не прорахує layout. Тому він перестраховується і бере оригінал. next/image генерує 8+ варіантів розмірів при білді і виводить елемент <picture> зі srcset дескрипторами. Браузер обирає потрібний варіант ще до початку завантаження, орієнтуючись на w дескриптори і підказку sizes. Жодних зайвих байтів.
Коли що використовувати
- Hero та зображення вище згину: додай
priority, щоб відключити lazy loading і вставити<link rel="preload">в<head> - Галереї і картки: стандартне відкладене завантаження з
sizesпід реальну ширину (наприклад,"(max-width: 768px) 100vw, 33vw") - Зображення з невідомими розмірами при білді:
fillвсередині позиціонованого батьківського контейнера - Зовнішні URL з Cloudinary, Unsplash, GitHub: налаштуй
remotePatternsуnext.config.js - Динамічні зображення зі станом завантаження:
placeholder="blur"зblurDataURLдля плавного переходу
Як це працює під капотом
При білді Next.js використовує Sharp для генерації WebP, AVIF і JPEG варіантів у кількох розмірах (приблизно 25%, 50%, 75%, 100% від оригіналу плюс пристроєві брейкпоінти). Результати кешуються у .next/cache/images. Для placeholder="blur" кодується крихітна base64 blur-мініатюра. На runtime компонент виводить <picture> з MIME-типізованими srcset записами: спочатку image/avif, потім image/webp, потім оригінальний формат. Браузер обирає через Accept заголовок.
AVIF дає приблизно 75% економії відносно JPEG. WebP - близько 30-50%. Next.js пробує AVIF першим через content negotiation, потім WebP, потім JPEG. ISR і SSG використовують той самий кеш, тому повторні запити не потрапляють до Sharp.
Типові помилки
Відсутність width і height
// Помилка: немає оптимізації, layout shift, помилка в консолі
<Image src="/me.jpg" alt="Me" />
// Правильно: завжди вказуй розміри
<Image src="/me.jpg" alt="Me" width={800} height={600} />fill без позиціонованого батьківського контейнера
// Помилка: зображення зникає або виходить за межі
<div>
<Image src="/photo.jpg" fill alt="Photo" />
</div>
// Правильно: батьківський елемент потребує position: relative і явних розмірів
<div className="relative w-full h-64">
<Image src="/photo.jpg" fill alt="Photo" className="object-cover" />
</div>Зовнішній URL без remotePatterns
// Кидає помилку в рантаймі: "hostname is not configured under images"
// Виправлення в next.config.js:
module.exports = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' }
]
}
}priority на всіх зображеннях. Це завантажує все заздалегідь, нівелює lazy loading і уповільнює початкове завантаження сторінки. priority потрібен лише для LCP елемента. На більшості сторінок це одне зображення.
sizes="100vw" для grid-елементів. Картка в сітці з трьох колонок рендериться приблизно у 33vw на десктопі. sizes="100vw" змушує браузер завантажувати 1200px варіант там, де достатньо 400px. Встанови sizes під реальну ширину рендеру.
Де зустрічається в реальних проектах
- Vercel.com: hero-банери з
priorityі breakpoint-специфічнимиsizes - Stripe docs: галереї скриншотів з
fillі blur placeholder - TailwindUI: картки продуктів з Cloudinary через
remotePatterns - GitHub: аватари з
avatars.githubusercontent.com, налаштовані вremotePatterns - Звичайний
<img>підходить для SVG-іконок і base64 даних до 10KB, або коли требаunoptimized={true}щоб повністю пропустити pipeline
Можливі питання на співбесіді
Q: Як next/image обирає між AVIF і WebP?
A: Перевіряє Accept заголовок браузера. Якщо в ньому є image/avif, видає AVIF (приблизно 75% економії відносно JPEG). Немає - пробує WebP. Якщо і WebP не підтримується, повертає оригінальний формат.
Q: Що реально робить проп sizes?
A: Підказує браузеру, якої ширини буде зображення на кожному брейкпоінті, ще до завантаження CSS. Без нього браузер вважає, що зображення займає 100% екрана, і завантажує більший варіант ніж потрібно. Для зображення шириною 300px у сайдбарі sizes="300px" суттєво зменшить завантаження.
Q: Яка різниця між placeholder="blur" і placeholder="empty"?
A: blur показує розмитий base64 прев'ю поки завантажується повне зображення. empty не показує нічого. blur підходить для великих hero-зображень, де видимий стан завантаження покращує сприйняту швидкість. empty краще для дрібних мініатюр, де розмитий прев'ю виглядав би гірше ніж просто порожнє місце.
Q: Чому next/image іноді суттєво збільшує час білду?
A: Sharp обробляє кожне унікальне зображення у кожному варіанті розміру. Галерея з 200 зображень і 8 брейкпоінтами дає 1600 операцій Sharp. Допомагає images.minimumCacheTTL - кешовані варіанти переживуть деплой. І вибіркове використання priority щоб контролювати що обробляється наперед.
Q: (Senior) У тебе ISR сторінки з динамічними product-зображеннями в S3. Оптимізація запускається на request time, але потрібна відповідь до 100ms. Як вирішити?
A: Два підходи. Перший: налаштуй кастомний loader який веде на CloudFront з Lambda@Edge для ресайзу на льоту; Next.js пропускає власний Sharp, CDN бере на себе обробку. Другий: обмеж кількість варіантів через deviceSizes і imageSizes у next.config.js, потім прогрій кеш пост-деплойним скриптом який проходить по кожному ISR шляху. На Vercel директорія .next/cache/images зберігається між деплоями за замовчуванням.
Приклади
Базовий: картка профілю з локальним зображенням
import Image from 'next/image'
export default function ProfileCard({ user }) {
return (
<div className="flex items-center gap-4">
<Image
src="/avatars/default.png"
alt={user.name}
width={64}
height={64}
className="rounded-full"
/>
<p>{user.name}</p>
</div>
)
}
// Next.js видає WebP варіант 64x64; оригінал лишається на диску незміненимwidth і height відповідають реальному розміру рендеру, тому зайвого завантаження немає. priority не потрібен, цей компонент ніколи не буде вище згину.
Середній: картка блог-посту з адаптивним fill
import Image from 'next/image'
export default function BlogCard({ post }) {
return (
<div className="relative w-full h-64">
<Image
src={post.coverImage}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover rounded-lg"
placeholder="blur"
blurDataURL={post.blurDataURL}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50" />
</div>
)
}
// Мобільний: завантажує ~390w WebP
// Планшет: ~640w WebP
// Десктоп у сітці трьох колонок: ~400w WebPПроп sizes тут і є головним. Без нього кожен пристрій завантажує найширший варіант. З ним мобільний отримує файл розміром під мобільний.
Просунутий: зовнішнє зображення з кастомним loader для Unsplash
// next.config.js
module.exports = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' }
]
}
}import Image from 'next/image'
// Кастомний loader передає параметри власного resize API Unsplash
function unsplashLoader({ src, width, quality }) {
return `${src}?w=${width}&q=${quality || 75}&auto=format`
}
export default function UnsplashPhoto({ id, alt }) {
return (
<Image
loader={unsplashLoader}
src={`https://images.unsplash.com/photo-${id}`}
alt={alt}
width={1200}
height={800}
quality={75}
/>
)
}
// URL на виході: https://images.unsplash.com/photo-123?w=1200&q=75&auto=format
// Кастомний loader обходить Sharp в Next.js; ресайзом займається CDN UnsplashПроп loader замінює стандартний URL-білдер Next.js. Зручно коли джерело зображень має власний resize API (Cloudinary, Imgix, Unsplash). Конфіг remotePatterns потрібен навіть з кастомним loader.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.