инструкция от lead back-end разработчика

Как оптимизировать работу с картинками на сайте или в приложении?

Если вам кажется, что загрузить картинку на сайт – это пара пустяков, наша статья для вас. Мы расскажем, почему длина пути от добавления изображения в админку до вывода его на экран зависит от того, какое качество вы хотите видеть в результате. 
Возможно, вам случалось сталкиваться с такими косяками:

  • долгая загрузка картинки
  • на айфоне плохое качество изображения
  • на фотографии обрезало кому-то голову
  • баннер на десктопе красивый, а не телефоне не видно контактных данных

Знайте, там пошли по наименьшему пути сопротивления и не уделили должного внимания способам загрузки изображения на сайт или в приложении. Поэтому расскажем, как наша команда работает с картинкам в проектах.
Дисклеймер: статья написана в феврале 2022 года. Использованы актуальные инструменты для этого времени.
Где хранить изображения для сайта?
Для хранения картинок мы используем S3. Это такой протокол для работы с файлами, который раньше ассоциировался только с сервисами Amazon, но сейчас его можно развернуть самому, или использовать много готовых решений. Например, в том же Яндексе. Фишка S3 хранилищ, что они “резиновые” и условно бесконечные. Если в приложении постоянно добавляются новые картинки, а вы не представляете, сколько их будет, и не хотите следить за свободным местом на диске, постоянно масштабировать его, то хранилище S3 – пока лучшее решение. Вы платите только за то, сколько места используете.

Кстати, особенно актуальна эта проблема для сервисов и e-comm. В нашей практике случалось, что ночью обновлялся каталог, и появлялись десятки гигабайт фотографий, а место на сервере заканчивалось. Используя S3, можно застраховать себя от этого.
Как ресайзить картинки для веба или приложений?
Для работы с картинками мы используем https://imgproxy.net. Он у нас развернут как отдельный микросервис и присутствует в каждом проекте. Ранее мы делали ресайз картинок на PHP, так как это наш основной стек, потом перешли на node.js, но по итогу решили, что https://imgproxy.net – более гибкий вариант, написанный на языке Go, с кучей настроек и высокой скоростью работы. Что еще круто, есть не только ресайз, но и водяные знаки, центрирование и много чего другого – смотрите на сайте.
Как подготовить картинку для веба?
В вебе мы используем 2 формата картинок: webp и jpg. Webp на данный момент – наиболее современный из стабильных форматов, который поддерживают почти все браузеры. Он предполагает хорошее качество и небольшой вес. Для тех, кто не поддерживает webp, грузим jpg.

Еще есть разные разрешения. Мы ориентируемся на 3 контрольных размера: мобайл, десктоп и планшет. Правильнее для каждого разрешения использовать свою картинку, чтобы телефон получал нужный для него размер и подгружал ровно то, что требуется ему, а десктоп – себе. Бывает же такое, что на одном устройстве картинка 300 на 400, а на другом – 100 на 100.

Помимо этого есть такой показатель как плотность точек на экране. Например, вы слышали про retina экраны в Apple. Так вот сейчас плотность пикселей на один физический пиксель бывает и 2, и 3. Это можно проверить, например, здесь. Выходит, что нам требуется дополнительно делать подходящие для каждого экрана картинки со своей плотностью.

В итоге мы получаем целое комбо изображений, которое формируется в зависимости от браузера, разрешения, плотности экрана. Например, есть карточка товара в каталоге. Для одной такой позиции мы генерируем 3 разрешения (десктоп, планшет, мобайл), 2 формата (jpg и webp) и подходящие по плотности экрана 1X, 2X, 3X картинки.
Фронтенд составляет требования для картинок таким образом.
А на бэкенде мы формируем целый массив картинок.
Как результат 1 загруженная картинка на примере каталога на выходе имеет 48 вариантов картинок различных размеров, разрешений и форматов.

Вот как выглядит компонент на react.
const vars = { 
    breakpoints: { 
        mobile: 500, 
        tablet: 1024, 
    },
}

export default function Picture({ desktop, tablet, mobile }) { 
    const desktopImages = desktop || {} 
    const { 
        x1: desktop_x1, 
        x2: desktop_x2, 
        webp_x1: desktop_webp_x1, 
        webp_x2: desktop_webp_x2, } = desktopImages 
    
    const tabletImages = tablet || {} 
    const { 
        x1: tablet_x1, 
        x2: tablet_x2, 
        webp_x1: tablet_webp_x1, 
        webp_x2: tablet_webp_x2, } = tabletImages 
    
    const mobileImages = mobile || {} 
    const { 
        x1: mobile_x1, 
        x2: mobile_x2, 
        webp_x1: mobile_webp_x1, 
        webp_x2: mobile_webp_x2, } = mobileImages 
        
    return desktop_x1 && desktop_x1.endsWith('.svg') ? ( 
        <img src={desktop_x1} alt="" /> 
    ) : ( 
        <picture> 
            <source 
                type="image/webp" 
                media={`(min-width: ${vars.breakpoints.tablet + 1}px)`} 
                srcSet={`${desktop_webp_x1}, ${desktop_webp_x2} 2x`} 
            /> 
            <source 
                media={`(min-width: ${vars.breakpoints.tablet + 1}px)`} 
                srcSet={`${desktop_x1}, ${desktop_x2} 2x`} 
            /> 

            <source 
                type="image/webp" 
                media={`(min-width: ${vars.breakpoints.mobile + 1}px)`} 
                srcSet={`${tablet_webp_x1}, ${tablet_webp_x2} 2x`} 
            /> 
            <source 
                media={`(min-width: ${vars.breakpoints.mobile + 1}px)`} 
                srcSet={`${tablet_x1}, ${tablet_x2} 2x`} 
            /> 
            
            <source 
                type="image/webp" 
                media={`(max-width: ${vars.breakpoints.mobile}px)`} 
                srcSet={`${mobile_webp_x1}, ${mobile_webp_x2} 2x`} 
            /> 
            <source 
                media={`(max-width: ${vars.breakpoints.mobile}px)`} 
                srcSet={`${mobile_x1}, ${mobile_x2} 2x`} 
            /> 
            
            <img src={desktop_x1} srcSet={`${desktop_x2} 2x`} alt="" /> 
        </picture> 
    )
}
Как подготовить картинку для мобайла?
Ситуация в целом очень схожа с вебом. Повторяться не будем. Но есть особенности с поддержкой форматов. Например, webp поддерживает ios>15 версии, android – с 11.
Так как приложения мы делаем только на react native, компонент со стороны react native выглядит следующим образом.
export default function PDImage({ urls, ...props }) {
    const getUri = () => { 
        const PD = PixelRatio.get() 
        if (PD <= 1 ) return urls.x1 
        if (PD > 1 && PPI <= 2) return urls.x2 
        if (PD > 2) return urls.x3 
    } 
    
    return ( 
        <Image 
            source={{ 
                uri: getUri(), 
            }} 
            {...props} 
        /> 
    )
}
Какого качества должны быть картинки для веба и мобайла?

В imgproxy, который мы используем для ресайза картинок, можно определять процент качества от исходного. Здесь следует найти баланс, ведь уменьшая этот параметр, вы уменьшаете и объем, но, соответственно, снижаете степень детализации. А если поставить 100% качество, получается “тяжелое” изображение.

Опытным путем мы для себя выбрали качество для webp 85%, для jpg – 95%.
Как кэшировать изображения?
На выходе получается, что для одной картинки мы имеем десятки изображений разных форматов и разрешений. Где все их хранить? Один из способов – при загрузке или появлении картинки в системе создавать сразу все необходимые варианты и статично хранить в S3. 

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

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

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