У сучасних вебзастосунках медіаресурси формують від 50% до 80% сумарного обсягу переданих мережею даних. Неоптимізовані растрові зображення є головним джерелом деградації метрик Core Web Vitals: затримки відтворення першого змістовного елемента (LCP (Largest Contentful Paint)) та стрибків верстки (CLS (Cumulative Layout Shift)).
Класичний підхід до оптимізації вимагає складного інфраструктурного пайплайна: пакетної генерації десятків роздільних здатностей для екранів із різною щільністю пікселів (Retina / DPR), ручної розмітки атрибута srcset, конвертації вихідних файлів у формати нового покоління (WebP, AVIF) та координації лінивого завантаження через IntersectionObserver.
Фреймворк Next.js вирішує це завдання декларативно завдяки вбудованому компоненту next/image. Він інкапсулює серверний пайплайн стиснення, запобігає зсувам верстки завдяки автоматичному резервуванню aspect-ratio та реалізує ліниве завантаження на рівні нативних браузерних API.
Архітектура next/image базується на розподіленій моделі трансформації за вимогою (On-Demand Transformation Pipeline). На відміну від статичного HTML-тега <img>, який передає файл браузеру у вихідній роздільній здатності та форматі, компонент Next.js генерує динамічний URL внутрішнього ендпоінта оптимізації:
/_next/image?url={SOURCE}&w={WIDTH}&q={QUALITY}
У рантаймі серверний процес задіює високопродуктивну C++ бібліотеку Sharp, стискає растровий потік, транскодує його у WebP або AVIF залежно від заголовка Accept у запиті браузера та кешує готовий двійковий артефакт у довгостроковому сховищі CDN.
Сервер рендерить <img> з атрибутами srcset, sizes та decoding='async'. Посилання спрямовані на ендпоінт /_next/image.
Якщо вказано priority, браузер негайно додає preload link із fetchpriority='high'. Інакше завантаження відкладається до потрапляння у в'юпорт.
Запит надходить на Edge CDN. Перевіряється наявність готового артефакту. На базі Accept обирається кодек (AVIF або WebP).
При першому зверненні сервер завантажує оригінал у буфер, стискає через бібліотеку sharp і зберігає результат у кеш.
Браузер плавно замінює розмиту заглушку placeholder='blur' завантаженим зображенням без найменшого зсуву верстки (CLS = 0).
При статичному імпорті локального графічного файлу Next.js автоматично інспектує метадані зображення під час збірки. Фреймворк витягує геометричні розміри (width, height) і генерує інлайн-заглушку BlurDataURL.
import Image from 'next/image';
import avatarPic from '@/public/images/avatar.jpg';
export function UserProfile() {
return (
<div className="flex items-center gap-4 p-4 rounded-xl border border-border bg-bg-surface">
<div className="relative w-16 h-16 rounded-full overflow-hidden border border-border">
<Image
src={avatarPic}
alt="Аватар користувача"
placeholder="blur" // Автоматично використовує згенерований blurDataURL
sizes="64px"
className="object-cover"
/>
</div>
<div>
<h3 className="font-semibold text-text-primary">Олександр Мельник</h3>
<p className="text-xs text-text-muted">Lead Software Architect</p>
</div>
</div>
);
}fill#Для динамічних картинок із зовнішніх сховищ або CMS (S3, Cloudinary, Unsplash) геометричні розміри вихідного файлу під час збірки невідомі. У таких випадках застосовується властивість fill, за якої зображення абсолютно позиціонується відносно батьківського контейнера.
import Image from 'next/image';
interface HeroBannerProps {
imageUrl: string;
title: string;
}
export function HeroBanner({ imageUrl, title }: HeroBannerProps) {
return (
// Обов'язково: батьківський контейнер має відносне позиціонування та явну висоту
<div className="relative w-full h-[320px] md:h-[480px] overflow-hidden rounded-2xl bg-bg-elevated">
<Image
src={imageUrl}
alt={title}
fill
// Критично: повідомляємо браузеру точний відсоток ширини екрана на брейкпоінтах
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 90vw, 1200px"
className="object-cover transition-transform duration-500 hover:scale-105"
priority // Вимикає lazy-loading та додає preload у head для LCP
quality={80}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent p-6 flex items-end">
<h1 className="text-2xl md:text-4xl font-bold text-white tracking-tight">{title}</h1>
</div>
</div>
);
}next.config.ts#Для запобігання атакам типу Server-Side Request Forgery (SSRF) та несанкціонованому використанню оптимізатора як відкритого проксі всі зовнішні джерела мають бути явно задекларовані у файлі конфігурації.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
// Увімкнення сучасних алгоритмів стиснення
formats: ['image/avif', 'image/webp'],
// Мінімальний час утримання в кеші оптимізованих файлів (31 день)
minimumCacheTTL: 2678400,
// Контроль нарізки брейкпоінтів для запобігання роздуванню кешу
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Захист від XSS-уразливостей при обробці SVG
dangerouslyAllowSVG: false,
contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
// Білий список довірених віддалених хостів
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'cdn.agenticlab.dev',
port: '',
pathname: '/uploads/**',
},
],
},
};
export default nextConfig;Розробник розмістив зображення в сітці карток товарів розміром 250px через <Image src={url} fill alt='Product' /> без зазначення атрибута sizes. До якого наслідку це призведе в мобільному браузері?
blurDataURL для динамічних зображень#Для віддалених зображень режим placeholder="blur" вимагає ручної передачі рядка blurDataURL. На сервері в Server Components можна згенерувати ультралегкий LQIP (Low Quality Image Placeholder) за допомогою утиліти на базі sharp.
import 'server-only';
import sharp from 'sharp';
export async function getDynamicBlurPlaceholder(imageUrl: string): Promise<string> {
try {
const response = await fetch(imageUrl);
if (!response.ok) throw new Error(`Failed to fetch image: ${response.statusText}`);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// Стискаємо зображення до мініатюри 10px із сильним розмиттям
const resizedBuffer = await sharp(buffer)
.resize(10)
.webp({ quality: 20 })
.toBuffer();
return `data:image/webp;base64,${resizedBuffer.toString('base64')}`;
} catch {
// Безпечний fallback: нейтральний сірий піксель 1x1
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
}
}Якщо медіафайли зберігаються на спеціалізованому медіа-CDN, вигідніше делегувати оптимізацію провайдеру, оминаючи сервер Next.js та заощаджуючи ліміти безсерверних трансформацій.
export interface ImageLoaderProps {
src: string;
width: number;
quality?: number;
}
export function cloudinaryLoader({ src, width, quality }: ImageLoaderProps): string {
// Видаляємо префікс домену, якщо передано абсолютний URL
const cleanSrc = src.replace(/^https?:\/\/[^/]+\//, '');
const q = quality || 75;
return `https://res.cloudinary.com/agenticlab/image/upload/f_auto,c_limit,w_${width},q_${q}/${cleanSrc}`;
}Чому встановлення priority={true} на всіх 20 картках каталогу товарів призведе до різкої деградації Core Web Vitals та TTFB?
Використання властивості fill без декларації sizes — найпоширеніша помилка верстки. Браузер не знає реальної ширини контейнера і за замовчуванням припускає найгірший сценарій: width: 100vw. У результаті на мобільному пристрої або всередині маленької картки браузер завантажує важке зображення з роздільною здатністю 1920px або 4K.
Рішення: Завжди явно описуйте поведінку елемента в сітці: sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw".
Додавання прапорця priority до зображень нижче лінії згину (below the fold) погіршує загальну швидкодію застосунку. Браузер впроваджує <link rel="preload"> і спрямовує мережевий канал на завантаження невидимих картинок, блокуючи виконання критичних JS-бандлів, стилів та шрифтів.
Рішення: Використовуйте priority виключно для 1–2 головних елементів першого екрана, які формують показник LCP.
Векторний формат SVG являє собою XML-документ, який може містити шкідливий JavaScript-код (<script> або інлайн-обробники подій). Якщо користувачі завантажують аватарки у форматі SVG, відкриття такого файлу напряму через оптимізатор виконає скрипт у контексті вашого домену, дозволяючи викрасти токени сесії.
Рішення: За замовчуванням у Next.js оптимізація SVG заблокована. Якщо SVG необхідний, вмикайте dangerouslyAllowSVG: true суворо в комбінації з contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;" та contentDispositionType: 'attachment'.
Властивість fill задає зображенню абсолютне позиціонування position: absolute; inset: 0. Якщо найближчий батьківський DOM-вузол не має явного CSS-позиціонування (relative, absolute або fixed), зображення спозиціонується відносно кореневого тега <body>, розтягнеться на всю сторінку та зламає геометрію сусідніх блоків.
Рішення: Батьківський контейнер для <Image fill ... /> обов'язково повинен містити клас relative у Tailwind CSS (або position: relative) та чітко задану висоту (h-[400px], aspect-video).
Під час збірки Next.js у Docker-контейнері з директивою output: 'standalone' бінарні файли бібліотеки sharp не включаються за замовчуванням. Next.js перемикається на резервний рушій WebAssembly (Squoosh), спричиняючи багаторазове зростання навантаження на процесор при кожному запиті зображення.
Рішення: У виробничому шарі Dockerfile явно встановіть бібліотеку: RUN pnpm add sharp, або додайте її в dependencies у package.json.
На платформі Vercel тарифікується кількість унікальних трансформацій зображень. Якщо не оптимізувати масиви deviceSizes та imageSizes у next.config.ts, випадкові розміри екранів користувачів створять тисячі унікальних варіантів, швидко вичерпавши тарифний ліміт.
Рішення: Обмежуйте масив images.deviceSizes лише реальними брейкпоінтами дизайну або використовуйте кастомний loader для передачі обробки на сторонні медіа-CDN.