Адаптивные изображения без шаманства

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

    На типичных сайтах изображения могут появляться тремя способами.
    1. Быть элементами дизайна сайта (бекграунды, кнопки и т.д.).
    2. Загружаться через специальные модули (например, изображения в фотоальбом).
    3. Вставляться через WYSIWYG-редактор CMS (например, в текст статьи).


    Мы захотели получить такое решение, которое было бы некой «надстройкой» над сайтом. Чтобы можно было не лезть в код CMS, через которую загружаются изображения на сайт, а также не готовить адаптивные картинки вручную.

    Сначала на помощь приходит реализация Adaptive Images

    Метод Adaptive Images


    Идея в том, чтобы с минимумом изменений в коде сайта предоставить эти самые адаптивные картинки. Алгоритм следующий:
    1. Небольшой яваскрипт записывает в куки максимальное значение из ширины/высоты устройства. Предполагается, что картинку больше данного размера показывать нет смысла.
    2. С помощью директивы в .htaccess идет рерайт всех картинок сайта на php-скрипт adaptive-images.php.
    3. В php-скрипте есть конфиг разрешений (связанный с media queries стилей). Значение из куки подгоняется под ближайшее в большую сторону значение из этого конфига. Если изображение по запрашиваемому пути существует и его ширина больше требуемой — изображение пережимается и кладется в специальную папку кеша (если оно не было пережато заранее).
    4. Скрипт отдает картинку клиенту.

    Плюсы Adaptive Images


    • Не требуется менять код сайта, кроме вставки одной строки js.
    • Не нужно пережимать картинки вручную, они сами пережмутся при необходимости.
    • Нет лишних клиентских запросов.
    • Поддерживается время жизни кешированных картинок — при обновлении оригинала рано или поздно кешированная будет обновлена.
    • Легко внедрить, так же просто откатить, при необходимости изменения размеров картинок все решается правкой одного массива.

    Минусы Adaptive Images


    А теперь немного о грустном. Данное решение подразумевает, что все (вообще все) картинки на сайте будет отдавать не nginx, не apachе, а php-скрипт. Каждая картинка — это запуск интерпретатора php (даже если картинка уже пережата). Это и медленно, и идеологически неверно.

    Мы захотели сохранить плюсы данного метода и избавиться от такого ужасного недостатка.

    Наш вариант


    Главная идея: не запускать php-скрипт, если изображение уже существует. Для этого apache в момент редиректа должен знать название пережатой под данное разрешение картинки. Это значит, что определение разрешения должно быть переложено с php на js. Таки образом js должен не просто вычислить максимальное значение из ширины/высоты устройства, а также определить требуемое разрешение, и именно его уже записать в куку.

    Также, чтобы apache мог проверить наличие картинки, он должен знать правило, по которому сохраняются пережатые изображения (в частности, название папки кеша), которое вообще говоря определено в php-скрипте.

    Тут мы, очевидно, теряем немного гибкости и получаем некое дублирование информации.
    • Массив разрешений должен быть продублирован в js-скрипт.
    • Папка кеша и правило сохранения картинок должно быть продублировано в .htaccess.

    Что получается


    Модификация js-скрипта: adaptive.js

    Здесь js, как и раньше, берет максимальное значение из высоты и ширины устройства. Определяет, есть ли модификатор плотности пикселей (ретина/не ретина), и на основе этих данных записывает в куки resolution.

    Инструкции по рерайту: .htaccess

    Рерайт стал чуть сложнее, но теперь проверяет наличие пережатой картинки до того, как обратиться к бекенду.

    RewriteCond %{REQUEST_URI} ^/upload/iblock/.+\.(?:jpe?g|gif|png)$
    Правило работает только на картинки из директории /upload/iblock/.

    RewriteCond %{REQUEST_FILENAME} -f
    Причем, только на реально существующие картинки, в отличие от оригинала.

    RewriteCond %{HTTP:Cookie} (^|;\ *)resolution=([1-9][0-9]+)
    Правило сработает только при наличии цифровой куки resolution. Если ее нет, веб-сервер отдаст оригинал изображения.

    RewriteRule .* /images_adaptive/%2%{REQUEST_URI} [L]
    Осуществляем переход в папку с кешированными изображениями, полагая, что эта папка называется images_adaptive. Дальше следуют разрешение и запрашиваемый путь оригинала. То есть, если пришел запрос на /images/photo.jpg, а разрешение пользователя подсчитано как 1024, то адаптивная картинка будет расположена по пути /images_adaptive/1024/images/photo.jpg.

    RewriteCond %{REQUEST_URI} ^/images_adaptive/.+\.(?:jpe?g|gif|png)$
    Пришел запрос на адаптивную картинку — рерайт с предыдущего правила, или прямой запрос, которого, между прочим, быть не должно. То есть, нигде ссылки прямо на эту папку, естественно, ставить нельзя.

    RewriteCond %{REQUEST_FILENAME} !-f
    Если такого файла все еще нет, то есть картинка еще не была пережата в нужный размер (и тут мы убиваем ненужные запросы в php при повторных обращениях к картинке).

    RewriteRule .* ai.php [L]
    Направляем запрос в наш php-скрипт, который найдет, пережмет при необходимости, и отдаст нужную картинку.

    Скрипт-обработчик запросов: ai.php

    Убрано определение разрешения. Нужно помнить, что в этом скрипте массив разрешений должен совпадать с аналогичным в adaptive.js, а путь к папке кеша должен совпадать с используем в правиле .htaccess.

    Минусы нашего форка


    • Избыточность хранимых данных. Если у нас есть одна картинка и 5 желаемых размеров, в которые ее нужно пережать, то сервер будет хранить в самом плохом случае все 6 изображений (оригинал и 5 копий). При этом, даже если картинку пережимать не нужно (скажем, она 300×100, а минимальное разрешение 480), то картинка все равно будет «пережата», то есть скопирована, 5 раз. Под каждое разрешение, чтобы избежать отдачи статики через php.
    • Обновление адаптивных изображений. Когда оригинал картинки обновится, скрипт-обработчик ничего об этом не узнает. Тут надо думать, подходит ли это для каждого конкретного случая, и как с этим бороться. Периодически очищать кеш вовсе, или что-то еще.
    • Дублирование информации в php, js и .htaccess


    Решение не стало идеальным, но несмотря на минусы, мы добились желаемого результата, освободив фронтенд-разработчиков от лишней работы.
    Кельник 34,94
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 19
    • +2
      Вариант с nginx location internal не рассматривали?
      • 0
        Порт на nginx планируем, пока без необходимости было, тут он не стоял в качестве проксировщика. По идее без проблем переписываться на nginx должно.
        • 0
          Я про то, что можно проверять наличие нужного размера и если необходимо ресайзить в php а потом делать «волшебный» редирект 'X-Accel-Redirect' и nginx сам будет отдавать картинку
          • 0
            Это хорошая идея, но это если nginx стоит. В целом один раз отдать каждую картинку прямо скриптом — не так уж страшно, и не надо завязываться еще и в php на то, кто там отдает nginx или apache. Apache что-то подобное умеет?
            • 0
              Про apache не знаю, вряд ли. Nginx нужен лишь в режиме обратного прокси.
      • 0
        Nginx умеет ресайзить крупать и кешировать.
        • 0
          image_filter вот тут про это писали: http://habrahabr.ru/post/157457/ оно вам не подходит?
          • 0
            Для нагруженных решений, как тут (https://speakerdeck.com/bobrik/node-dot-js-for-millions-of-images) есть смысл морочиться. А в условиях разработки сайтов в рамках интернет-агенств все чуть по-другому :D
          • 0
            Правда Ваша, но далеко не все хостят свои проекты там, где nginx
            a) есть
            б) собран с ngx_http_image_filter_module
            в) есть возможность менять его конфиг

            И вообще нет уверенности, что этот модуль кеширует то, что жмет. Больше похоже, что он это делает на лету на каждый файл.
            • 0
              Ну да, кеш надо дополнительно настраивать. Nginx все это может сделать, но не у всех он есть.
              • 0
                Кеширует если настроить — проверено. Натсройка всего там строчек на 10, разобраться по документации очень просто.
                Просто решение совершенно безгеморойное. Можно например настроить 1 сервер для кучи проектов, сделать там поддомены img.site1.com img.site2.com.
                А далее обращаться как-то вот так: img.site1.com/200x400/background_big_image.jpg и все остальное разруливать яваскриптом.

                Хотя конечно это все не прокатит, если речь идет о куче мелких сайтов, которые просто передаются заказчику.
                • 0
                  О том и речь. Сайты, мб, и не очень маленькие, но заказчики их во многих случаях забирают, и там уже действительно «не прокатит».
            • +1
              1. Можно не копировать, а прокидывать симлинки. Оно и быстрее будет.
              2. К имени файла картинки невредно добавлять номер версии. В тех (редких) случаях, когда вообще подразумевается замена картинок. Сходу и не придумаю, зачем это может быть нужно.
              3. Как вариант, из php парсить .htaccess и писать в js. Ориентируясь на дату обновления, например. Если .htaccess моложе .js Для простоты нужный кусок .htaccess сделать include.
              • 0
                1. Спасибо, да, стоит сделать так.
                2. Редко бывает, поэтому не заморачивались на эту тему
                3. Не охота на каждый вызов php читать лишний файл
              • 0
                Правило работает только на картинки из директории /upload/iblock/.

                Битрикс?
                Если так, то почему не воспользовались стандартным методом CFile::ResizeImageGet()? Он вам и картинку создаст нужных размеров и сам ее найдет при втором обращении, а при обновлении оригинала обновит и измененные изображения при новых обращениях.
                • 0
                  Потому что Битрикс — это частный случай

                  Мы захотели получить такое решение, которое было бы некой «надстройкой» над сайтом. Чтобы можно было не лезть в код CMS, через которую загружаются изображения на сайт, а также не готовить адаптивные картинки вручную.
                • 0
                  почему бы в таком случае не переложить работу веб-сервера на js? Сгенерировать урл(ну типа там resized_image_1024_768.jpg), попытаться его загрузить(на том же js), на fail — уже отправить запрос напрямую в php и получить сгенерированную картинку?
                  и ещё дальше — почему бы не переложить работу по сжиманию картинки на js? В том числе и с отправкой кэша на сервер(рисково правда давать права на запись)?
                  И не надоест ли клиенту ждать пока его картинки подгрузятся?
                  PS с пережимом картинок сам сталкивался и писал небольшую функцию которая отслеживает все связи. Это была фото-галерея на собственном движке, нужно было хранить мини-картинку, побольше превьюху, ещё больше одну и оригинал. Делал всё на загрузке картинки в базу. Правда там было известно что где показывать.
                  • 0
                    В чем профит переложения на js опроса на существования картинки? Минусы понятны, плюсы — нет.

                    Как js должен сжимать? Сначала загрузить полную, потом пережать на клиенте и отдать нам? Грузите все, что хотите?))

                    Пережатие картинки происходит раз для каждого разрешения и не так долго, как можно подумать.
                    • 0
                      >> В чем профит переложения на js опроса на существования картинки?
                      Меньше нагрузка на веб-сервер, меньшее количество настроек, потенциально — меньше шансов запороть всю систему с помощью кривых рук.
                      Ну и не вижу особой разницы между получением 404 и редиректом с JS и тем же, но реализованным на стороне сервера. Хоть в случае с JS действительно будет медленнее за счёт обслуживания http.
                      >> Как js должен сжимать?
                      canvas -> scale. Очень быстро сжимает прямо на GPU. Профит в том что время серверное вообще не тратиться.
                      О поддержке canvas можете не заикаться — необходимый функционал работает везде, кроме ie 8(пользователи которого не оценят ваш адаптивный дизайн).
                      Кроме того, возможно реализовать любой «интеллектуальный» алгоритм сжатия.
                      >> Сначала загрузить полную, потом пережать на клиенте и отдать нам? Грузите все, что хотите?))
                      Если нет уже сжатой картинки — скачать полную и сжать. После этого(или вместе с этим) послать асинхронный запрос на сервер на сжатие этой картинки в кэш.
                      При такой схеме никаких проблем с правами на запись не будет.
                      Преимущества того или иного подхода нужно оценивать в реальных системах.
                      Я думаю если будет много графики и разных клиентов — имеет смысл распределить загрузку.
                      >> Пережатие картинки происходит раз для каждого разрешения и не так долго, как можно подумать.
                      А так, представьте себе, можно будет сжимать картинку не под определённые разрешния а под конкретно пользовательские!

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое