14. Оптимизация изображений
🖼️ next/image: изображения без боли
Заголовок раздела «🖼️ next/image: изображения без боли»Изображения — самая большая причина медленных сайтов. Необработанное изображение на 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: адаптивные изображения
Заголовок раздела «📐 sizes: адаптивные изображения»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: важные изображения
Заголовок раздела «🚀 priority: важные изображения»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: изображение заполняет контейнер
Заголовок раздела «🎨 fill: изображение заполняет контейнер»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> );}🌫️ placeholder blur: красивая загрузка
Заголовок раздела «🌫️ placeholder blur: красивая загрузка»Вместо пустого места при загрузке показываем размытое превью:
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==';🌍 Удалённые изображения: конфигурация
Заголовок раздела «🌍 Удалённые изображения: конфигурация»Для загрузки изображений с внешних серверов нужно добавить их в конфиг:
/** @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;⚡ WebP и AVIF: автоматическая конвертация
Заголовок раздела «⚡ WebP и AVIF: автоматическая конвертация»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 logoexport function Logo() { return ( <Image src="/logo.svg" alt="Логотип компании" width={120} height={40} priority // логотип в шапке — видим сразу /> );}
// 3. Lazy gallery с blur placeholderexport 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> );}