lenec ru

← все посты

next/image на практике: как не получить раздутую галерею

17K

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-фон.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.