next/image на практике: как не получить раздутую галерею
Next/image — одна из тех штук, которые работают «как бы сами», и из-за этого в большинстве проектов настроены неправильно. Картинки приезжают в полном размере, sharp ест память, а LCP не проседает только потому, что картинки лежат под скроллом.
Расскажу, как я сейчас использую next/image в проде, какие настройки реально нужны, и где обычно теряется выигрыш.
Что делает компонент
На бэкграунде у Next.js есть встроенный image optimizer. Когда страница запрашивает картинку через next/image, она проксируется через /_next/image?url=&w=...&q=.... На лету картинка ресайзится, переводится в современный формат (AVIF или WebP), кешируется, отдаётся обратно.
Это работает автоматически на Vercel и в self-hosted-режиме (если есть sharp в зависимостях). Для статически сгенерированных картинок в public/ то же самое происходит на этапе сборки.
Минимальный пример
import Image from "next/image";
import hero from "./hero.jpg";
export function Hero() {
return (
<Image
src={hero}
alt="Закат над городом"
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
);
}
Когда импортируешь картинку из файла — Next сам знает её ширину и высоту, ставит правильные width и height атрибуты, и браузер резервирует место заранее. CLS равен нулю.
Если URL внешний — нужно указывать размер вручную:
<Image
src="https://cdn.example.com/photo.jpg"
alt=""
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 800px"
/>
Без width и height Next ругается. Это правильно: иначе браузер не знает место, и страница прыгает при загрузке.
sizes: главный атрибут, который все забывают
Если есть один атрибут, который реально важен — это sizes. Он говорит браузеру, какой размер картинка займёт на разных вьюпортах. Из этого вычисляется, какую версию запросить с сервера.
// плохо: без sizes Next считает, что картинка занимает 100vw
<Image src={hero} alt="" />
// хорошо: на мобиле 100% ширины, на десктопе максимум 800px
<Image
src={hero}
alt=""
sizes="(max-width: 768px) 100vw, 800px"
/>
Без sizes на ноутбуке грузится та же огромная картинка, что и на 4K-мониторе. На 1080p мониторе она в два раза больше нужного. Бандл-трафик растёт впустую.
Я для каждого Image сразу пишу sizes, даже если кажется лишним. Это пять секунд работы и несколько сотен килобайт экономии.
fill vs width/height
Иногда картинка должна занимать определённый блок, и точные размеры заранее неизвестны (например, карточка товара с фиксированной высотой 200px и переменной шириной). Используй fill:
<div style={{ position: "relative", aspectRatio: "4/3" }}>
<Image
src="/photo.jpg"
alt=""
fill
sizes="(max-width: 768px) 100vw, 33vw"
style={{ objectFit: "cover" }}
/>
</div>
Картинка заполняет родителя, у которого position: relative. Размеры родителя определяются дизайном (через aspect-ratio, например), а Next выбирает оптимальный размер исходника.
priority: только для LCP-элементов
Атрибут priority заставляет картинку грузиться без lazy loading. Для одной-двух картинок «выше сгиба» это нужно — они и есть LCP-элемент.
На остальных картинках priority ставить не надо. Lazy loading по умолчанию включён, и это правильное поведение. Картинка под скроллом не нужна для первого экрана.
Самая частая ошибка, которую я вижу: priority на каждой картинке в галерее. Тогда смысл lazy loading теряется, страница тащит сразу 30 картинок.
placeholder="blur"
Полезный приём для больших картинок: пока полная не загрузилась — показывать размытую миниатюру.
<Image
src={hero}
alt=""
placeholder="blur"
/>
Если картинка импортируется из файла — Next сам генерирует blur. Если внешняя — нужно передать blurDataURL:
<Image
src={remoteUrl}
alt=""
width={1200}
height={800}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
На бэке заранее генерируешь base64-миниатюру из исходника. Я обычно это делаю на этапе загрузки на CDN, чтобы не считать на каждый запрос.
Конфигурация для внешних доменов
Если используешь внешние URL — нужно прописать домены в next.config.js:
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
pathname: "/images/**",
},
],
},
};
Без этого Next откажется проксировать. Это защита от того, чтобы твой image optimizer не использовали как открытый прокси для любых картинок мира.
Форматы: AVIF или WebP
Next по умолчанию умеет в WebP. AVIF включается отдельно:
module.exports = {
images: {
formats: ["image/avif", "image/webp"],
},
};
AVIF на большинстве картинок весит на 20–30% меньше WebP. Минус — кодирование медленнее, и при self-hosted это может бить по CPU. На Vercel это не твоя забота, на своих серверах — тестируй.
Пять граблей из моей практики
1. Картинка-логотип в header через next/image
Если логотип всегда 32x32 и он в каждом маршруте — ставь его как обычный img или SVG. Накладные расходы на оптимизацию для такой картинки не окупаются.
2. Не указали sizes на картинке внутри grid
Картинка занимает треть ширины на десктопе и всю на мобиле, но без sizes грузится в полном размере. Грузится 1200px-версия, а отображается на 400px. Лечится одной строкой.
3. Поставили priority на все «важные» картинки
«Все они важные». Нет, ровно одна — та, что определяет LCP. Остальные — lazy.
4. Использовали fill без position: relative у родителя
Картинка ломает layout. Next ругается в консоли, в продакшене ругань не видно, проблема всплывает у пользователя.
5. Внешний CDN без remotePatterns
Картинки не отображаются. Ошибка в консоли, в логах. Лечится одной строкой в конфиге, но если забыл — ищи минут пять.
Когда не использовать next/image
- SVG-иконки. Никаких преимуществ, лишний DOM-обёртка.
- Очень маленькие декоративные картинки (под 5 КБ). Накладные расходы оптимизации не окупаются.
- Динамические превью с canvas или WebGL. Это уже не картинки в обычном смысле.
- Очень динамичный фон, который меняется на каждом ховере.
В остальных случаях — берём.
Замер влияния
На одном из проектов перед оптимизацией картинок:
- LCP: 3.2 секунды.
- Total page weight: 4.1 МБ.
- Картинок в первом экране: 7.
После того, как все картинки получили правильный sizes, формат AVIF и нормальные placeholder="blur":
- LCP: 1.8 секунды.
- Total page weight: 1.4 МБ.
- Картинок в первом экране: те же 7.
Ничего не убирали из дизайна. Просто настроили компонент правильно.
Что запомнить
next/image на 80% настраивается двумя атрибутами: sizes и priority. sizes — обязательно везде, без исключений. priority — только на одной-двух картинках, которые видны сразу.
Для внешних URL не забывай прописать remotePatterns в конфиге. Для локальных импорт через import — самый удобный способ, потому что Next сам знает всё про размер и плейсхолдер.
И не оптимизируй то, что не нужно. SVG-иконку в SVG, огромное хиро — через next/image, маленький декоративный градиент — нативный img или CSS-фон.