open atlas
↑ К треку
Next.js с нуля до senior NEXT · 05 · 01

next/image: конвейер ресайза, srcset и цена каждого варианта

next/image рендерит img со srcset из on-demand AVIF/WebP-вариантов через /_next/image, лениво грузит ниже фолда и требует размеры — раскладка не прыгает. priority помечает LCP-картинку; remotePatterns закрывает оптимизатор; каждый вариант стоит CPU или денег по счётчику.

NEXT Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Ритейлер перезапускает витрину на Next.js и празднует — пока кто-то не прогоняет Lighthouse на среднем Android через 4G: LCP 5,9 секунды. Hero-картинка — JPEG на 2,4 МБ шириной 4000 пикселей, одинаково отдаваемый и телефону с экраном 360 пикселей, и 4K-монитору, и вдобавок лениво загружаемый, потому что сниппет <img loading="lazy"> копипастили везде «для производительности». Починка занимает один вечер: переход на next/image, priority на hero — и оптимизатор отдаёт 640-пиксельный AVIF весом 38 КБ вместо 2,4 МБ. LCP падает до 1,8 с, мобильная конверсия заметно подрастает. Через три недели финансовый отдел спрашивает про новую строку в счёте Vercel: оптимизатор перемолол все 40 000 фотографий каталога, каждую в нескольких ширинах, а оптимизация изображений тарифицируется по исходным картинкам. Никто не дочитал до места, где волшебный компонент оказывается ещё и поверхностью биллинга. next/image — обе эти истории сразу; урок ровно о том, что он делает и сколько стоит каждый вариант.

Что компонент рендерит на самом деле: srcset плюс эндпоинт оптимизатора

next/image — это не «умный <img>» в рантайме: он рендерит обычный <img>, но со сгенерированным srcset, каждый кандидат которого указывает на встроенный маршрут оптимизатора: /_next/image?url=<источник>&w=<ширина>&q=75. Ширины берутся из конфига: deviceSizes (по умолчанию 640, 750, 828, 1080, 1200, 1920, 2048, 3840) для полноширинных картинок и imageSizes (по умолчанию от 16 до 384) для маленьких фиксированных. Кандидата выбирает браузер — не Next — по атрибуту sizes, который задаёте вы; именно поэтому картинка с fill без sizes молча качает на телефон десктопный вариант: по умолчанию предполагается 100vw самого широкого вьюпорта.

Первый запрос для тройки (url, width, quality) делает настоящую работу: оптимизатор (sharp на self-hosted сервере) забирает источник, ресайзит и перекодирует в лучший формат из заголовка Accept браузера — сначала AVIF, потом WebP, потом исходный формат. Результат кешируется на диске в .next/cache/images и переотдаётся, пока его не вытеснит minimumCacheTTL (или upstream Cache-Control). Основная экономия байтов живёт в переговорах о формате: WebP обычно на 25–35% меньше JPEG того же качества, AVIF — ещё на ~20–30% меньше WebP; тот hero, ставший из 2,4 МБ 38 КБ, — это ресайз и формат вместе.

import Image from "next/image";
import hero from "./hero.jpg"; // статический импорт: width/height выводятся автоматически, blurDataURL генерируется

export default function Home() {
  return (
    <>
      <Image src={hero} alt="Spring collection" priority sizes="100vw" />
      <Image
        src="https://cdn.example.com/sku/8841.jpg" // удалённый URL: размеры — ваша ответственность
        alt="Linen shirt"
        width={400}
        height={500}
        sizes="(max-width: 768px) 50vw, 400px"
      />
    </>
  );
}

Компромисс: трансформация on-demand означает, что вы не генерируете заранее варианты, которые никому не понадобятся, — но и то, что первый посетитель каждого варианта оплачивает латентность кодирования (AVIF-кодирование крупного источника съедает сотни миллисекунд CPU), а очищенный кеш — например, свежий деплой без персистентного .next/cache — снова делает первым посетителем каждого.

Размеры убивают CLS; priority спасает LCP

Компонент отказывается рендериться без width и height (или fill внутри спозиционированного родителя с размерами) — и это не бюрократия, а механизм борьбы с CLS. Зная размеры заранее, браузер резервирует бокс через aspect-ratio до прихода первого байта картинки, и текст ниже не прыгает, когда она загрузится. Cumulative Layout Shift выше 0,1 на p75 проваливает Core Web Vitals, а картинки без размеров — исторически его главная причина. Классический провал с fill: родитель без position: relative и реальной высоты схлопывается в ноль или выпускает картинку из раскладки — проп переложил ответственность за размеры на ваш CSS, а CSS трубку не взял.

Поведение загрузки — второй контракт. Каждый next/image по умолчанию loading="lazy" — правильно для 30 карточек товара ниже фолда и катастрофично для hero. Ленивая LCP-картинка ждёт раскладку, потом intersection observer, потом качается с обычным приоритетом; эта цепочка стабильно добавляет секунду и больше к LCP, чей «хороший» порог — 2,5 с. Проп priority переворачивает всё: eager-загрузка, fetchpriority="high" и preload-подсказка, чтобы запрос стартовал вместе с документом. Правило, которое senior-команды держат на ревью: LCP-элемент каждой шаблонной страницы получает priority, и больше никто — пять preload-ов значат, что приоритета нет ни у кого.

Почему это работает

Почему Next делает lazy умолчанием, если это сжигает LCP на hero-картинках? Потому что статистически большинство картинок на странице — ниже фолда, и жадная загрузка их всех конкурирует за канал с той единственной, которая важна. Умолчание оптимизирует большинство ценой того, что одна — ваш LCP-элемент — требует явной, осознанной аннотации. Next даже помогает ловить ошибки: в dev-режиме он предупреждает, когда картинка, видимая при загрузке, не имеет priority. Честная ментальная модель: lazy — безопасное умолчание для стада, priority — ручной тумблер, который вы должны ровно одной картинке на вьюпорт.

Викторина

Hero рендерится через next/image с корректными width/height и без других пропов. Полевые данные: LCP 4,6 с на мобильных; сама картинка весит всего 45 КБ. Самая вероятная причина?

remotePatterns и счёт: оптимизатор — поверхность атаки и затрат

Любой удалённый src обязан совпасть с записью в images.remotePatterns (protocol, hostname, опционально port и pathname), иначе запрос получает 400 — Next отказывается оптимизировать хосты не из allowlist. Эта строгость — не педантизм. Ваш эндпоинт /_next/image забирает произвольные URL, гоняет на результате CPU-тяжёлое кодирование и кеширует выход: открытый allowlist (hostname: "**") превращает его в бесплатный прокси обработки изображений для всего интернета. Кто угодно может направить ваш эндпоинт на любую картинку где угодно — а платите вы: CPU на self-hosted машине, тарифицируемыми трансформациями на Vercel. Сужайте паттерны до конкретных хостов и префиксов путей (/sku/**) и относитесь к wildcard-хосту на ревью так же, как к cors: * на аутентифицированном API.

Модель затрат зависит от деплоя и меняет архитектуру. На Vercel оптимизация тарифицируется по исходным картинкам (в каждый план входит квота; каталог на 40 000 SKU пробивает включённые тысячи быстро, и каждая новая исходная картинка дальше — платная). Self-hosted — это CPU и диск: sharp ресайзит 4000-пиксельный JPEG порядка 100–300 мс CPU на вариант, кеш живёт в .next/cache/images, и этот каталог обязан переживать деплои — иначе вы переплачиваете кодирование всего каталога каждый релиз. Третий вариант выходит из конвейера совсем: кастомный loader переписывает URL на специализированный image-CDN (Cloudinary, imgix, Thumbor), сохраняя преимущества разметки next/image — srcset, размеры, lazy/priority — и перенося трансформацию на инфраструктуру, для этого построенную.

Боевой провал: команда хостится сама, деплоится ежедневно из свежего контейнера и не монтирует том под .next/cache/images. Каждый деплой p95 латентность картинок прыгает к ~900 мс, CPU ноды упирается в потолок на двадцать минут, пока sharp перекодирует длинный хвост каталога, — потом всё успокаивается до следующего деплоя. На мониторинге это ежедневная пила, которую никто не может объяснить, потому что регрессии нет ни в одном диффе.

Викторина

Чтобы разблокировать маркетинг, вставляющий ссылки на картинки откуда угодно, разработчик ставит remotePatterns с hostname '**'. На что только что подписался сайт?

Вспомните перед уходом
  1. 01
    Пройдите путь от <Image> в JSX до байтов в сети: что генерируется, кто выбирает, что выполняется на первом запросе?
  2. 02
    Почему width/height обязательны, зачем hero нужен priority и во что именно обходится wildcard в remotePatterns?
Итог

next/image — это генератор разметки плюс сервис оптимизации. Компонент рендерит обычный img, все кандидаты srcset (атрибут, перечисляющий изображения под разные экраны) которого указывают на /_next/image с url, шириной из deviceSizes или imageSizes и качеством (по умолчанию 75); браузер выбирает между ними по вашему атрибуту sizes — поэтому пропущенный sizes на полноширинной картинке молча скачивает десктопные байты на телефон. Оптимизатор работает на первом запросе каждого варианта: проверка remotePatterns, загрузка источника, ресайз sharp и перекодирование в лучший формат из Accept — AVIF (формат нового поколения, меньше WebP на 20–30%), затем WebP, складывая экономию 25–35% и ещё 20–30% относительно JPEG, — затем кеш в .next/cache/images, каталог, который обязан переживать деплои, иначе весь каталог товаров перекодируется каждый релиз ежедневной пилой CPU. Размеры обязательны, потому что они и есть механизм CLS: width и height (или fill внутри спозиционированного родителя с размерами) дают браузеру зарезервировать бокс до прихода байтов и удержать вас под порогом 0,1. Ленивая загрузка — умолчание, правильное для всего ниже фолда, но LCP-картинка обязана нести priority — eager-запрос, fetchpriority high, preload — иначе она ждёт раскладку и intersection-детект и утаскивает LCP за линию 2,5 с. Наконец, оптимизатор — поверхность затрат и атаки: сужайте remotePatterns до конкретных хостов и путей, потому что wildcard превращает эндпоинт в публичный прокси обработки изображений, и знайте свою модель затрат — потарифно по исходным картинкам на Vercel, CPU и персистентный диск self-hosted, либо кастомный loader, сохраняющий преимущества разметки и делегирующий трансформацию специализированному image-CDN. Теперь, когда встретишь LCP выше 2,5 с на Next.js-сайте, первое, что проверяешь — несёт ли hero priority; первое, что проверяешь в неожиданном счёте Vercel — что разрешает remotePatterns.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.