Symfony Css rewrite: image to base64

keltanas

marty cats
Гоняя один макет в PageSpeed и находясь в поисках путей оптимизации обнаружил, что у меня в нем много мелких картинок используются в качестве фонов. Из за чего их не засунешь в спрайты. Решение нашлось в кодировании их в base64 и вставке в css.

В статье описываются некоторые инструменты, облегчающие этот процесс. Однако, это либо какие-то сервисы, либо sass/compas, ruby/jammit. А макет-то на обычном css и Symfony.

Вот и родилась идея написать фильтр для assetic (аналогично cssrewrite), который бы мне реплэйсил изображения в base64.

Вот примерный код класса, реализующего такой фильтр:
PHP:
<?php
/**
 * Encode images to css data:base64
 * @author: keltanas <[email protected]>
 */

namespace FT\SiteBundle\Component\Assetic\Filter;

use Assetic\Asset\AssetInterface;
use Assetic\Filter\BaseCssFilter;

class CssImageBase64Filter extends BaseCssFilter
{
    protected $maxSize = 2048;

    /**
     * @param int $maxSize
     */
    public function setMaxSize($maxSize)
    {
        $this->maxSize = $maxSize;
    }

    /**
     * @return int
     */
    public function getMaxSize()
    {
        return $this->maxSize;
    }

    /**
     * Filters an asset after it has been loaded.
     *
     * @param AssetInterface $asset An asset
     */
    public function filterLoad(AssetInterface $asset)
    {
    }

    /**
     * Filters an asset just before it's dumped.
     *
     * @param AssetInterface $asset An asset
     */
    public function filterDump(AssetInterface $asset)
    {
        $sourceBase = $asset->getSourceRoot();
        $sourcePath = $asset->getSourcePath();

        $path = dirname($sourceBase . DIRECTORY_SEPARATOR . $sourcePath);
        $content = $this->filterUrls($asset->getContent(), function($matches) use ($path) {
            if (preg_match('@url\([\\\'" ]*(?:data|http|\/\/).*?[\\\'" ]*\)@u', $matches[0])) {
                return $matches[0];
            }
            $img_path = realpath($path . DIRECTORY_SEPARATOR . $matches['url']);
            if (file_exists($img_path) && filesize($img_path) < $this->getMaxSize()) {
                $is = getimagesize($img_path);
                return 'url("data:'.$is['mime'].';base64,'.base64_encode(file_get_contents($img_path)).'")';
            }
            return $matches[0];
        });

        $asset->setContent($content);
    }

}
Чтобы подключить, надо добавить класс в качестве сервиса:
PHP:
parameters:
    assetic_base64_filter_class: FT\SiteBundle\Component\Assetic\Filter\CssImageBase64Filter
services:
    assetic_base64_filter:
        class: %assetic_base64_filter_class%
        tags:
            - { name: assetic.filter, alias: cssrewriteb64 }
        calls:
            - [setMaxSize, [4096]]
А при подключении в лэйауте ставить фильтр перед cssrewrite (а то после него пути уже не будут работать):
PHP:
    {% stylesheets  'bundles/ftsite/css/bootstrap.css'
                    'bundles/ftsite/css/style.css'
                    filter='?cssrewriteb64'
                    filter='cssrewrite'
                    filter='?yui_css'
                    output="css/style.css" %}
    <link rel="stylesheet" href="{{ asset_url }}" />
    {% endstylesheets %}
Собственно, выставляя параметр setMaxSize на нужное количество байт, можем выставить верхнюю границу размера изображений, до которой картинки будут кодироваться в CSS.
Благодаря такой конфигурации, мы в debug режиме можем преспокойно работать с обычными картинками. А в no-debug режиме (на продакшене) дампить файл с уже перезаписанными в base64 картинками. И не надо придумывать лишних инструментов.

Готов к любым критике, предложениям, пожеланиям. Может кто-то подскажет как улучшить решение.
Обкатав скрипт его можно будет и в бандл завернуть.

ЗЫЖ не смотря на простоту, мне не удалось найти уже готового фильтра.
ЗЫЖ2 Хотя библиотека Assetic, для которой написан фильтр, не является напрямую частью symfony. Однако, это наиболее популярный фреймворк, использующий её. Но, фильтр можно использовать в любых проектах, в том числе написанных не на symfony.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Гоняя один макет в PageSpeed и находясь в поисках путей оптимизации обнаружил, что у меня в нем много мелких картинок используются в качестве фонов. Из за чего их не засунешь в спрайты. Решение нашлось в кодировании их в base64 и вставке в css.

Готов к любым критике, предложениям, пожеланиям. Может кто-то подскажет как улучшить решение.
Обкатав скрипт его можно будет и в бандл завернуть.

ЗЫЖ не смотря на простоту, мне не удалось найти уже готового фильтра.
Заебись, ты отказался от асинхронных запросов к картинкам и их кеширования в пользу увеличения размера их размера, размера css файла, дополнительного парсинга и декодирования и перезагрузке всех картинок при изменении css файла?
 

keltanas

marty cats
флоппик ты этот глупый комментарий с хабра скопипастил? На практике все эти утверждения безосновательны.
Картинки в CSS кешируются, гзипятся и освобождают от проблем загрузки мелких файлов.
К тому же, если внимательно посмотришь пост, то поймешь, что парсится CSS только 1 раз при деплое.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
К тому же, если внимательно посмотришь пост, то поймешь, что парсится CSS только 1 раз при деплое.
Ага, а картинки обратно кодируются на каждое отображение в бинарный формат чем, магией добра?
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Картинки в CSS кешируются, гзипятся и освобождают от проблем загрузки мелких файлов.
Изменишь CSS, и будешь вынужден перекачать ВСЕ эти картинки заново. Независимо от того, есть ли они на текущей странице, или нет. Причем, это base64, он как минимум, на треть больше чем обычный размер.

А твои "проблемы" с мелкими картинками, мало того, что существуют только при первом запросе (и только они все на одной странице), решаются установкой SPDY.
 

hell0w0rd

Продвинутый новичок
Но и css нужно будет перезагружать. Как то я здесь задавал вопрос зачем вообще кодировать в base64 - я так понял что если запрос за картинкой дороже увеличения размера файла - ничего страшного.
На счет размера... После gzip все такая же разница?
Да и как эти вопросы к фильтру относятся?)
PS мне кажется решением проблемы с перезагрузкой фонов, при изменении css - может быть изменение структуры css файла.
То есть все base64 картинки выносятся в отдельный файл и добавляются к загрузке. Таким образом если у нас более 1 такой картинки - на лицо снижение кол-ва запросов и отказ от перезагрузки всех картинок, в случае обновления css
из минусов - если уж картинка изменится - прийдется грузить все заново, но это уже и минус спрайта
А на счет бандла - мне кажется можно смело форкнуть assetic и попробовать отправить пулл-реквест, нужная фича
 

AmdY

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

hell0w0rd

Продвинутый новичок
у меня разрыв шаблона, спрайты как раз для этого и предназначены или я что-то не так понял в задаче?
оО и как сделать через спрайт фон? Он для мелких иконок предназначен, где можно ограничить no-repeat
 

MiksIr

miksir@home:~$
оО и как сделать через спрайт фон? Он для мелких иконок предназначен, где можно ограничить no-repeat
От ситуации зависит. Если это фон для небольшого блока, то можно нарисовать его по одной координате, а повторять по другой только. Тогда можно укладывать спрайты.

Заебись, ты отказался от асинхронных запросов к картинкам и их кеширования в пользу увеличения размера их размера, размера css файла, дополнительного парсинга и декодирования и перезагрузке всех картинок при изменении css файла?
Ты не совсем прав. Если подходить к этому с умом - это выгоднее за счет экономии на HTTP заголовках. В случае обычных сайтов, где нет никаких доменов для статики - там еще и куки ходят, так что приличных размеров HTTP ответ может быть.

Удобно это может быть и с точки зрения создания отдельных блоков при всяких БЭМ идеологиях. Например, кнопка - если ей нужны картинки, это скорее всего копеешные размеры. Но если объединять их в спрайты с другими элементами - то потеряем независимость этого модуля. Это, правда, моя теория лишь, на практике мы пока только шрифты загоняем в css, но тут другие причины.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Ты не совсем прав. Если подходить к этому с умом - это выгоднее за счет экономии на HTTP заголовках.
Ты хочешь сказать, что перекодировка в base64 занимает меньше места, чем http заголовки? Ну и - один файл = один поток, картинки же браузер будет получать асинхронно, в несколько потоков (в шесть потоков по дефолту в большинстве браузеров). В итоге мы проигрываем в скорости получения клиентом контента.
 

MiksIr

miksir@home:~$
Ты хочешь сказать, что перекодировка в base64 занимает меньше места, чем http заголовки? Ну и - один файл = один поток, картинки же браузер будет получать асинхронно, в несколько потоков (в шесть потоков по дефолту в большинстве браузеров). В итоге мы проигрываем в скорости получения клиентом контента.
Так я и говорю - вдумчиво. Если заголовков у нас около 300 байт, то base64 будет выгоден для файлов меньше килобайта. А килобайт - это даже много для всяких мелких картинок. Плюс еще оверхед на сам запрос. Браузеру обычно есть чем занять свои потоки даже кроме кучи мелких картинок, так что один css с мелкими картинками, имхо, быстрее проскочит, а значит меньше эти потоки будет занимать. Другое дело, что я никогда не сталкивался с сайтами, где "много мелких бекграундов, и все тянутся" ;) А просто иконки - спрайтами.
 

hell0w0rd

Продвинутый новичок
85K Jun 15 14:57 test.css
64K Jun 15 14:55 test.css.gz
63K Jun 15 14:49 test.jpg.gz
63K Jun 15 14:58 test.orig.jpeg
Сжимал вот эту картинку
Так где как минимум увеличение размера на 1/3?:)
Ну и учитывая размер картинок для которых это нужно изначльно - это вообще не аргумент
 

keltanas

marty cats
флоппик
Тут можно сколько угодно обсасывать тему со всех сторон и предлагать какие-то не мыслимые модули.
Я провел тесты, где страница с 10 картинок (<1k каждая) грузилась в раздельном режиме 3-4 сек, в составе css 0.6 сек.
Собственно тесты, представленные в указанной статье на хабре также показывают прирост скорости, так что предлагаю взять из как предмет обсуждения.
Если есть основания полагать, что это не так, прошу предметно опровергнуть.

А тут сейчас еще всякие Ragazzo набегут и будут кричать, что модули для симфони - это гавно.

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

AmdY
Скажем, если нам надо сделать фон, который множится по вертикали и горизонтали, то в спрайт его поместить будет сложно.
Да и чем лучше использование спрайта? Здесь мы можем вести разработку на обычных картинках, а при деплое загнать мелкие изображения в css. Суть та же. Уменьшить число запросов. А будет этот спрайт идти картинкой или гзипованным css? В чем существенная разница?
 

keltanas

marty cats
Вот небольшие тесты по сжатию:
PHP:
$base64 name.png | gzip > name.gz

-rw-rw-r-- 1 keltanas keltanas   937 июня  15 18:14 contacts-bg-sep.gz
-rw-rw-r-- 1 keltanas keltanas   872 мая    2 01:47 contacts-bg-sep.png
-rw-rw-r-- 1 keltanas keltanas   165 июня  15 18:09 news-bg-bt.gz
-rw-rw-r-- 1 keltanas keltanas   123 мая    2 00:50 news-bg-bt.png
-rw-rw-r-- 1 keltanas keltanas   170 июня  15 18:12 news-bg-top.gz
-rw-rw-r-- 1 keltanas keltanas   128 мая    2 00:50 news-bg-top.png
-rw-rw-r-- 1 keltanas keltanas   855 июня  15 18:13 panel1-bg-common.gz
-rw-rw-r-- 1 keltanas keltanas   794 мая    1 23:50 panel1-bg-common.png
На совсем мелких картинках получается действительно увеличение размера на 1/3. При картинках около 1к увеличение размера до 10%. Причина в том, что мелкие base64 кодированные файлы почти не жмутся.
Так что пусть каждый решает за себя, надо это ему или нет. По результатам моих происков скорость загрузки увеличивается.
 

keltanas

marty cats
Таки нашел фильтр PhpCssEmbedFilter в Assetic, который выполняет функции сабжа. Работает с помощью скрипта phpCssEmbed
Однако, этот фильтр не позволяет указать, какие файлы надо реплейсить, а какие нет. Так что если в моем варианте добавить проверку на абсолютные url, то у него будет преимущество в виде фильтра по размеру.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Ты замечательно опустил мои замечания про асинхронность, кеширование отдельных изображений, выкачивание всех изображений, даже отсутствующих на текущей странице. Да и твои 10% - это 10% помноженные на число клиентов, если что.
Мое дело на проблемы указать, а что вы с этой информацией сделаете - мне все равно.
 

keltanas

marty cats
флоппик
Я благодарю тебя за проявленный интерес и замечания. Но, тем не менее, мои тесты (и тесты других людей) говорят о том, что вся эта асинхронность не дает прироста.
Согласен, если бы шла речь об изображениях хотя бы по 100к (да что там, по 10к, хотя надо тестировать), то асинхронность и раздельный кеш изображений были бы более производительными.
Да и если речь будет идти о юзерпиках десятков лимонов юзеров, никто их не будет пихать в один спрайт. Речь о мелких картиночках, которые проще грузануть одним 50-100к файлом. И, как правило картинки, нужные для разметки, нужны на странице все за редким исключением.

Если у тебя много разных лэйаутов с разными изображениями, не надо держать разметку всех лэйаутов в одном css. Можно так же разделить по назначению. И в каждом лэйауте подключать только то, что надо. В случае с модулем assetic можно указать, какие css собирать и в каком файле сохранять. Помести картинки от лэйаута1 в layout1.css, а картинки от лэйаута2 в layout2.css.
Данный способ от дурака не спасет, как и любой другой инструмент.
Если подходить с умом, можно получить необходимый профит.
 
Сверху