Skip to main content

Оптимізація зображень (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 помилка

Швидкий приклад

tsx
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

tsx
// Помилка: немає оптимізації, layout shift, помилка в консолі <Image src="/me.jpg" alt="Me" /> // Правильно: завжди вказуй розміри <Image src="/me.jpg" alt="Me" width={800} height={600} />

fill без позиціонованого батьківського контейнера

tsx
// Помилка: зображення зникає або виходить за межі <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

js
// Кидає помилку в рантаймі: "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 зберігається між деплоями за замовчуванням.

Приклади

Базовий: картка профілю з локальним зображенням

tsx
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

tsx
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

js
// next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'images.unsplash.com' } ] } }
tsx
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?