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

Изображения — обычно самый тяжёлый контент на странице (50-70% трафика). Оптимизация изображений — самый простой способ ускорить сайт.

Зачем нужно

Средняя веб-страница весит ~2MB, из которых ~1MB — изображения. Неоптимизированные изображения: замедляют LCP, расходуют трафик пользователя, увеличивают время загрузки на мобильных. Правильная оптимизация может ускорить сайт в 2-3 раза.

Где используется

Все сайты с изображениями: e-commerce, блоги, лендинги, SPA, портфолио. Особенно критично для мобильных пользователей.

Предпосылки

HTML, CSS, Web Vitals, Browser rendering flow

Форматы изображений

Формат Сжатие Прозрачность Анимация Поддержка Для чего
JPEG Lossy Нет Нет 100% Фото
PNG Lossless Да Нет 100% Иконки, скриншоты
WebP Both Да Да 97% Замена JPEG/PNG
AVIF Lossy Да Да 92% Лучшее сжатие
SVG Vector Да Да 100% Иконки, логотипы
GIF Lossless 1-bit Да 100% Простые анимации

WebP — основной выбор

JPEG 100KB → WebP ~70KB (на 25-35% меньше при том же качестве)
PNG  200KB → WebP ~120KB (на 30-40% меньше)

AVIF — будущее

JPEG 100KB → AVIF ~50KB (на 50% меньше!)
Но: медленнее кодируется, поддержка ~92%

Когда какой формат

Контент Формат Почему
Фото WebP / AVIF Лучшее сжатие
Иконки, логотипы SVG Масштабируется, маленький размер
Скриншоты с текстом WebP (lossless) / PNG Чёткий текст
Простые иконки SVG / CSS Не нужен растр
Фоновые паттерны SVG / CSS gradients Бесконечное масштабирование

Сжатие

Инструменты

# Sharp (Node.js) — самый популярный
npm install sharp

# Squoosh CLI
npm install @squoosh/cli

# imagemin
npm install imagemin imagemin-webp imagemin-avif

Sharp — конвертация и сжатие

const sharp = require('sharp');

// JPEG → WebP
await sharp('photo.jpg')
  .webp({ quality: 80 })      // 80% качества
  .resize(800, 600)            // Ресайз
  .toFile('photo.webp');

// PNG → AVIF
await sharp('screenshot.png')
  .avif({ quality: 50 })
  .toFile('screenshot.avif');

// Batch-обработка
const fs = require('fs');
const path = require('path');

const inputDir = './images';
const outputDir = './optimized';

fs.readdirSync(inputDir).forEach(async (file) => {
  if (file.match(/\.(jpg|jpeg|png)$/i)) {
    await sharp(path.join(inputDir, file))
      .webp({ quality: 80 })
      .toFile(path.join(outputDir, file.replace(/\.\w+$/, '.webp')));
  }
});

Responsive Images (srcset / sizes)

<!-- ПЛОХО: одно изображение для всех экранов -->
<img src="hero-2000px.jpg" alt="Hero">
<!-- Мобильный телефон загружает 2000px изображение! -->

<!-- ХОРОШО: браузер выбирает подходящий размер -->
<img
  src="hero-800.jpg"
  srcset="
    hero-400.jpg   400w,
    hero-800.jpg   800w,
    hero-1200.jpg 1200w,
    hero-2000.jpg 2000w"
  sizes="
    (max-width: 600px) 100vw,
    (max-width: 1200px) 50vw,
    800px"
  alt="Hero image">

Как это работает

srcset — список доступных размеров (400w = 400px ширина)
sizes  — подсказка, сколько места изображение займёт на экране

Браузер выбирает оптимальный вариант, учитывая:
- Ширину viewport
- Плотность пикселей (Retina 2x, 3x)
- sizes подсказку

Picture element — разные форматы

<picture>
  <!-- Браузер выберет первый поддерживаемый формат -->
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="Фото" width="800" height="600">
</picture>

<!-- Разные изображения для разных экранов -->
<picture>
  <source media="(max-width: 600px)" srcset="photo-mobile.webp">
  <source media="(max-width: 1200px)" srcset="photo-tablet.webp">
  <img src="photo-desktop.webp" alt="Фото" width="1200" height="800">
</picture>

Lazy Loading

<!-- Нативный lazy loading -->
<img src="photo.jpg" loading="lazy" alt="Фото" width="400" height="300">

<!-- НЕ делай lazy loading для LCP-изображения! -->
<img src="hero.jpg" loading="eager" alt="Hero" fetchpriority="high">
// Intersection Observer для продвинутого lazy loading
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
}, { rootMargin: '200px' }); // Начать загрузку за 200px до появления

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

CDN для изображений

<!-- Cloudinary — обработка на лету -->
<img src="https://res.cloudinary.com/demo/image/upload/w_400,f_auto,q_auto/photo.jpg">
<!--
  w_400   — ширина 400px
  f_auto  — автоформат (WebP для Chrome, JPEG для Safari)
  q_auto  — автокачество
-->

<!-- imgix -->
<img src="https://myapp.imgix.net/photo.jpg?w=400&auto=format,compress">

Preload для LCP-изображения

<head>
  <!-- Предзагрузка LCP-изображения — критически важно! -->
  <link rel="preload" as="image" href="hero.webp" type="image/webp">

  <!-- С srcset -->
  <link rel="preload" as="image"
    imagesrcset="hero-400.webp 400w, hero-800.webp 800w"
    imagesizes="100vw">
</head>

SVG-оптимизация

# SVGO — оптимизатор SVG
npx svgo icon.svg -o icon.min.svg

# Результат: удаляет метаданные, комментарии, лишние атрибуты
# Обычно -30-50% размера
<!-- Инлайн SVG для мелких иконок (экономим HTTP-запрос) -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  <path d="M12 2L2 7l10 5 10-5-10-5z"/>
</svg>

Частые ошибки

1. Огромные изображения без ресайза

<!-- ПЛОХО: 4000x3000 фото для превью 200x150 -->
<img src="original-4000x3000.jpg" style="width: 200px;">

<!-- ХОРОШО: серверный ресайз + srcset -->
<img src="thumb-200x150.webp" srcset="thumb-400.webp 2x" style="width: 200px;">

2. Нет width/height → CLS

<!-- ПЛОХО: без размеров — скачок при загрузке (CLS) -->
<img src="photo.jpg">

<!-- ХОРОШО: размеры заданы — место зарезервировано -->
<img src="photo.jpg" width="800" height="600">

3. Lazy loading для LCP

<!-- ПЛОХО: LCP-изображение загружается лениво -->
<img src="hero.jpg" loading="lazy">

<!-- ХОРОШО: eager + preload -->
<img src="hero.jpg" loading="eager" fetchpriority="high">

4. Только один формат

<!-- ПЛОХО: только JPEG -->
<img src="photo.jpg">

<!-- ХОРОШО: progressive enhancement -->
<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="Фото">
</picture>

Практика

  1. Конвертируй 5 JPEG-изображений в WebP через Sharp — сравни размеры
  2. Добавь srcset и sizes для hero-изображения
  3. Используй <picture> для AVIF → WebP → JPEG fallback
  4. Настрой lazy loading для всех изображений ниже fold
  5. Найди LCP-элемент на своём сайте и добавь <link rel="preload">

Связанные темы

Ресурсы