Оптимизаторы картинок не работают из коробки

Введение

Многие разработчики уже хорошо знают, что картинки одно из мест на сайте, которое надо оптимизировать. Формат webp лучше png, а avif лучше jpeg - очевидно же! А что еще очевидно для многих, что если есть возможно подключить готовые оптимизаторы картинок, например, @nuxt/image для Nuxt, или next/image для Next, или любое другое готовое решение, то так и надо делать. Подключил, используешь нужный компонент, указываешь желаемый формат и все работает - ведь так?

А вот и нет! Не все так просто оказывается на практике. Опустим сейчас такой момент, что многие разработчики забывают, что помимо формата изображения обладают еще и другими параметрами, которые тоже стоит оптимизировать. В этой статье мы поговорим о том, что даже если вы хорошо потрудились над качественным сжатием картинок через оптимизаторы, то вы можете все равно потерять в скорости в том месте, о котором просто не задумываетесь.

Оптимизаторы картинок не работают из коробки

О чем не пишут в документациях к оптимизаторам картинок

За счет чего достигается оптимизация? - за счет размера, формата, качества и т.д. А как это достигается? Ведь оптимизатор, чтобы упростить разработчику жизнь, работает с оригиналами картинок и делает с ними то, что разработчик мог бы и в ручную, но у него на это нет времени. А именно ужимает, урезает, упрощает что может. На этом моменте внимательному читателю может уже стать очевидно, что ни одна работа не может даваться бесплатно и мгновенно. Оптимизация картинок - это тоже работа.

И если вы не позаимствовали для этой задачи где-то суперкомпьютер, то в зависимости от мощности арендованного сервера, вы можете как выиграть по скорости, так и проиграть. А если у вас еще и на сайт заходят живые люди и не по полтора человека в месяц - то будет ваш сервер страдать, а пользователи заваривать кофе пока ждут свою картинку.

И проблема тут возрастает от множества параметров: число пользователей, размер исходной картинки, сложность ее оптимизации, мощность сервера, текущая его нагрузка, скорость физического накопителя и т.д.

А что с этим можно сделать

Поговорили о проблеме, теперь о решении. И их тут несколько. Начнем с чего попроще.

Кеширование в браузере пользователя

Для начала можно сделать так, чтобы хотя бы у пользователя, который уже заходил на сайт, для него картинки не оптимизировались заново. И тут решение очевидное для многих - это добавление заголовка Cache-Control. Он сообщает браузеру, что такой-то маршрут надо закешировать на указанный срок. Вот только и тут есть подводный камень: если вы не обрабатываете картинку сборщиком или не добавляете к ее названию хэш иным способом, то в случае, если вы обновили картинку, но не изменили название - пользователь не увидит изменения. Но разберем эту проблему в рамках отдельной статьи. Если вы меняете картинки на своем сайте не часто, то решение вам частично подойдет.

Внедрить такое можно как на стороне SSR фреймворков, так и на стороне Web-сервера. Тема это достаточно популярна и даже если у вас специфичная технология, то вы всегда сможете найти способ, как добавить нужный заголовок. Приведу пару простых примеров.

На стороне SSR фреймворков, как правило, есть готовые встроенные настройки для такой задачи. Например, решение для nuxt/image:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    ipx: {
      maxAge: 31536000,
    },
  },
})

Или же можно добавить необходимые заголовки на стороне Web-сервера. Например, для Nginx:

application.conf
server {
  listen 8000;

  location / {
    proxy_set_header Host $host;
    proxy_pass http://client;
  }

  location ~* ^\/_ipx\/ {
    expires 1y;
    add_header Cache-Control "public, no-transform";
    etag off;
    if_modified_since off;

    proxy_set_header Host $host;
    proxy_pass http://client;
  }
}

Пример выше добавит необходимый заголовок для маршрутов /_ipx/, которые использует оптимизатор ipx. Для иного оптимизатора могут потребоваться иные маршруты.

Но почему выше было написано, что решение частичное?

Кеширование на стороне Web-сервера

При использовании заголовка кеширования если пользователь зайдет на страницу повторно, то для него действительно не будет происходить повторный процесс оптимизации. Да и процесс открытия изображения вообще будет мгновенным, т.к. не будет никакого запроса на сервер вообще. А потому решение выше никак не противоречит следующему. Далее будет приведен один из рецептов, поняв суть которого, вы легко можете придумать и альтернативные варианты, если предложенный мной вам подходить не будет.

Оптимизированные картинки можно сохранять на уровне Web-сервера, например, Nginx. Как правило, Web-сервера очень быстрые, а вес оптимизированных картинок достаточно мал, чтобы не бояться чуть-чуть занять память сервера. Суть решения следующая.

Когда первый пользователь зайдет на страницу, для него будет осуществлен полноценный запрос за изображением который дойдет до вашего оптимизатора. Ему придется немного подождать прежде чем он получит свою картинку. Но затем результат сжатия будет сохранен в память сервера. И когда уже второй пользователь посетит страницу, то ему уже ждать не прийдется ничего - он получит свою картинку намного быстрее из кеша сервера. И это же произойдет для всех последующих пользователей - никакого ожидания. А что еще великолепно, так это то, что ваш сервер будет более расслабленным без лишней нагрузки. Если ваше приложение использует Backend или SSR frontend, то и они могут стать быстрее.

Внедряется это максимально просто. Разберем пример для Nginx.

На стороне http блока нужно объявить кеш хранилище:

nginx.conf
http {
  # Тут будут и иные необходимые вам настройки

  proxy_cache_path /var/cache/nginx/ipx keys_zone=ipx:2000m inactive=30d;
}

Разберем чуть подробнее имеющуюся строку:

  • /var/cache/nginx/ipx - путь к хранилищу кеша. Можно указать и иной путь, но желательно использовать /var/cache/nginx директорию, потому что именно она предполагается для использования в качестве хранилища кешей.
  • ipx - это название вашего хранилища кеша. Запомните его, оно будет использоваться в следующей настройке.
  • 2000m - это объем хранилища. В данном случае 2000 мегабайт. Если оно будет превышено, то Nginx начнет удалять менее актуальный кеш. Так что за память сервера можно сильно не беспокоиться.
  • inactive=30d - это время хранения кеша. В данном случае 30 дней. Если кеш не будет запрашиваться в течение 30 дней, то он будет удален.

Когда кеш хранилище будет настроено, его можно будет использовать. Добавить его можно в блоке server:

application.conf
server {
  location ~* ^\/_ipx\/ {
    expires 1y;
    add_header Cache-Control "public, no-transform";
    etag off;
    if_modified_since off;

    proxy_cache_valid 1y;
    proxy_cache ipx;

    proxy_set_header Host $host;
    proxy_pass http://client;
  }
}

А это уже даже знакомый код. Я специально добавил к уже упомянутой настройке для заголовка кеширования новую, чтобы показать, что одно не противоречит другому и даже лучше использовать оба способа вместе.

Что тут имеется:

  • proxy_cache_valid 1y - это время хранения кеша. В данном случае 1 год. Через этот срок кеш будет обновлен / перепроверен принудительно.
  • proxy_cache ipx - это название вашего хранилища кеша. Оно должно совпадать с названием, которое вы указали в proxy_cache_path.

И все - не больно было, правда? Зато за такую проделанную работу пользователи в душе скажут спасибо, ибо ваш сайт станет шустрее. Ну и сервер будет доволен, т.к. работы станет меньше - а это еще и ваш счет за оплату услуг сервер провайдера.

Бонус

Если вы используете приложение в Docker, то, разумеется, не стоит забывать, что чтобы данные не потерялись при перезапуске контейнера, то директорию кеша картинок необходимо добавить в список volume вашего контейнера. Просто и легко вот так:

compose.yaml
services:
  nginx:
    image: application-nginx
    volumes:
      - ipx-cache:/var/cache/nginx/ipx

volumes:
    ipx-cache:

Или можно объявить хранилищем всю директорию /var/cache/nginx - это уже на ваше усмотрение.