Перейти к содержимому

14. Оптимизация изображений

Изображения — самая большая причина медленных сайтов. Необработанное изображение на 5 МБ может убить твои Core Web Vitals! Next.js компонент <Image> автоматически решает все проблемы: конвертирует в WebP/AVIF, создаёт responsive варианты, ленится загружать и резервирует место. Как профессиональный фотограф в одном компоненте 📸


import Image from 'next/image';
// Локальное изображение — Next.js знает размеры автоматически!
import profilePic from '@/public/images/profile.jpg';
export default function Avatar() {
return (
<Image
src={profilePic} // или строка: '/images/profile.jpg'
alt="Яша — разработчик" // ОБЯЗАТЕЛЬНО для a11y и SEO
// Размеры НЕ нужны для импортированных — определяются автоматически
/>
);
}
// Для строкового src — размеры ОБЯЗАТЕЛЬНЫ
export function Banner() {
return (
<Image
src="/images/banner.jpg"
alt="Баннер"
width={1200} // оригинальный размер
height={400} // оригинальный размер
/>
);
}

sizes говорит браузеру, насколько широким будет изображение при разных viewport’ах. Это позволяет браузеру выбрать оптимальный размер из srcset:

// Компонент карточки товара
export function ProductCard({ product }: { product: Product }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Image
src={product.image}
alt={product.name}
width={400}
height={300}
sizes="
(max-width: 768px) 100vw,
(max-width: 1024px) 50vw,
25vw
"
// Что это значит:
// • до 768px: изображение занимает 100% экрана
// • 768-1024px: 50% экрана (2 колонки)
// • 1024px+: 25% экрана (4 колонки)
// Next.js создаст srcset и браузер выберет нужный размер!
/>
</div>
);
}
// Hero изображение — всегда на всю ширину
export function HeroImage() {
return (
<Image
src="/hero.jpg"
alt="Главный баннер"
width={1920}
height={1080}
sizes="100vw" // всегда 100% viewport
priority // загружаем сразу (не lazy)!
/>
);
}

priority={true} отключает lazy loading и сигнализирует браузеру о высоком приоритете. Используй для изображений в зоне видимости при загрузке (above the fold):

// ✅ Используй priority для:
// • Hero images (главный баннер)
// • Логотип в шапке
// • Первое изображение в списке (LCP-элемент)
export default function HomePage() {
return (
<>
{/* LCP элемент — всегда priority! */}
<Image
src="/hero.jpg"
alt="Главная страница"
width={1920}
height={1080}
priority // ← убирает lazy loading, добавляет preload
sizes="100vw"
className="w-full h-auto"
/>
{/* Остальные изображения — без priority (lazy по умолчанию) */}
<Image src="/feature-1.jpg" alt="Фича 1" width={600} height={400} />
<Image src="/feature-2.jpg" alt="Фича 2" width={600} height={400} />
</>
);
}
// ❌ НЕ ставь priority везде — теряется смысл!
// Правило: не более 2-3 изображений с priority на странице

fill убирает обязательность width/height — изображение растягивается на весь родительский контейнер:

// ВАЖНО: родительский элемент должен иметь position: relative (absolute/fixed/sticky)!
export function ProductGallery({ images }: { images: string[] }) {
return (
<div className="grid grid-cols-3 gap-2">
{images.map((img, i) => (
<div
key={i}
className="relative aspect-square" // aspect-square = 1:1, position: relative
style={{ position: 'relative' }}
>
<Image
src={img}
alt={`Изображение ${i + 1}`}
fill
sizes="(max-width: 768px) 33vw, 25vw"
className="object-cover" // object-contain | object-cover | object-fill
style={{ objectFit: 'cover' }}
/>
</div>
))}
</div>
);
}
// Cover для фонового изображения
export function BackgroundImage({ src, children }: {
src: string;
children: React.ReactNode;
}) {
return (
<section className="relative h-96">
<Image
src={src}
alt="" // декоративное — пустой alt
fill
sizes="100vw"
className="object-cover"
priority
/>
{/* Оверлей поверх */}
<div className="relative z-10 h-full flex items-center justify-center bg-black/50">
{children}
</div>
</section>
);
}

Вместо пустого места при загрузке показываем размытое превью:

import Image from 'next/image';
// Вариант 1: автоматически для локальных изображений
import heroImage from '@/public/hero.jpg';
export function HeroWithBlur() {
return (
<Image
src={heroImage}
alt="Герой"
placeholder="blur" // Next.js сам создаёт blurDataURL для локальных!
priority
/>
);
}
// Вариант 2: для remote images — нужен blurDataURL вручную
export function RemoteImageWithBlur({ src, blurDataURL }: {
src: string;
blurDataURL: string;
}) {
return (
<Image
src={src}
alt="Remote image"
width={800}
height={600}
placeholder="blur"
blurDataURL={blurDataURL} // base64 миниатюра
/>
);
}
// Генерация blurDataURL на сервере:
import { getPlaiceholder } from 'plaiceholder';
async function getImageWithBlur(src: string) {
const buffer = await fetch(src).then(r => r.arrayBuffer());
const { base64 } = await getPlaiceholder(Buffer.from(buffer));
return base64;
}
// Или простой inline base64 placeholder (серый пиксель):
const PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';

Для загрузки изображений с внешних серверов нужно добавить их в конфиг:

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// Способ 1: паттерны (рекомендуется в Next.js 14+)
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: '**.amazonaws.com', // wildcard поддомена
},
{
protocol: 'https',
hostname: 'cdn.mysite.com',
port: '',
pathname: '/images/**',
},
],
// Форматы (по умолчанию: ['image/webp'])
formats: ['image/avif', 'image/webp'],
// Размеры для srcset
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Кэширование
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 дней
// Отключить оптимизацию (например, при самостоятельном хостинге без CDN)
// unoptimized: true,
},
};
export default nextConfig;

Next.js автоматически конвертирует изображения в современные форматы по Accept заголовку браузера:

Браузер поддерживает AVIF → отдаём AVIF (ещё меньше JPG на 50%)
Браузер поддерживает WebP → отдаём WebP (меньше JPG на 30%)
Старый браузер → отдаём оригинальный формат

Это происходит без изменений в коде! Просто используй <Image> и Next.js сделает всё сам.

// Изображение будет автоматически конвертировано:
<Image src="/photo.jpg" alt="Фото" width={800} height={600} />
// Chrome получит: /photo.jpg?w=800&q=75&f=webp (WebP/AVIF)
// Safari старый: /photo.jpg?w=800&q=75 (JPEG)

Без next/image:
├── Загрузка: 5.2 MB (оригинальный JPEG)
├── LCP: ~4.2s
├── CLS: 0.25 (прыжки макета без размеров)
└── FID: высокий (блокирующая загрузка)
С next/image:
├── Загрузка: 180 KB (WebP + нужный размер)
├── LCP: ~0.8s
├── CLS: 0 (зарезервированное место)
└── Lazy loading: остальные не блокируют

// 1. Аватар пользователя
export function UserAvatar({ src, name, size = 40 }: {
src: string;
name: string;
size?: number;
}) {
return (
<div style={{ position: 'relative', width: size, height: size }}>
<Image
src={src}
alt={`Аватар ${name}`}
fill
sizes={`${size}px`}
className="rounded-full object-cover"
style={{ borderRadius: '50%', objectFit: 'cover' }}
/>
</div>
);
}
// 2. Responsive logo
export function Logo() {
return (
<Image
src="/logo.svg"
alt="Логотип компании"
width={120}
height={40}
priority // логотип в шапке — видим сразу
/>
);
}
// 3. Lazy gallery с blur placeholder
export function ImageGallery({ photos }: { photos: Array<{ src: string; blur: string; alt: string }> }) {
return (
<div className="columns-3 gap-4">
{photos.map((photo, i) => (
<div key={i} className="relative mb-4 break-inside-avoid">
<Image
src={photo.src}
alt={photo.alt}
width={400}
height={0}
sizes="(max-width: 768px) 100vw, 33vw"
className="w-full h-auto"
style={{ height: 'auto' }}
placeholder="blur"
blurDataURL={photo.blur}
/>
</div>
))}
</div>
);
}