Вставка минифицированного css-кода в теге style в head на лету

Янв 5, 2017
Вставка минифицированного css-кода в теге style в head на лету

Рассказывает Денис Богданов – ведущий разработчик нашей компании.

Погоня за увеличением скорости загрузки страниц продолжается. Представим, что мы всё сжали, минифицировали, объединили в один файл, скрипты "опустили", используем svg-спрайты и всё в этом духе. Что дальше? Одно из трендовых инноваций сегодня (особенно актуально на мобильниках), это размещение стилей верхней части страницы прямо в тег head и асинхронная подгрузка остальных файлов. Данную рекомендацию сплошь и рядом пропагандируют все сервисы тестирования производительности, в том числе и гугловый, так как браузер не начнёт рендерить страницу и не покажет её пользователю, пока не получит все linkи в head. Статья на тему

В своём примере я не выделяю стили верхней части страницы, а подключаю весь закешированный битриксом файл стилей в теге head. Хочу сразу предупредить, что данный метод стоить тестировать на производительность на конкретном сайте, в некоторых случаях это может увеличить First byte из-за работы с большими строками через регулярные выражения, в некоторых сильно увеличит объём страницы и уменьшит объём кешируемых данных, что приведёт к обратному эффекту. 

/* Для замера скорости работы */
$start = microtime(true);

/* Получить пути подключенных файлов стилей */
$css_str = Bitrix\Main\Page\Asset::getInstance()->GetCSS();
preg_match_all('/href="(.*)"\stype/', $css_str, $matches);

/*
 * Перебираем результат, так как метод возвращает все подключаемые файлы стилей
 * нас интересует только кеш объединённых файлов, содержит в названии 'template_'
 */
foreach($matches[1] as $val)
{
    $val = explode('?', $val);
    $full_path = $_SERVER['DOCUMENT_ROOT'] . $val[0];
    /*
     * Производим подмену только для закешированного объединённого файла
     * если он существует и не пуст (на пустоту проверка дальше)
     */
    if (strpos($full_path, 'template_') !== false && file_exists($full_path))
    {
        $st yle = trim( file_get_contents($full_path) );
        if (!empty($style))
        {
            $search  = '<st yle type="text/css"></style>';
            $replace = '<st yle type="text/css">'. $style .'</style>';
            // Вставить стили
            $content = str_replace($search, $replace, $content);
            // Вырезить <li nk>
            $content = preg_replace('/<li nk\shref="'.  addcslashes($val[0], '/') .'(.*)>/', '', $content);
        }
    }
}

/* Вставляем время исполнения кода в заранее подготовленное место */
$content = str_replace('#time_replace#', microtime(true) - $start, $content);

Немного истории. Bitrix Framework буфферизирует весь клиентский код. Данный подход делает возможным работу отложенных функций и прочей манипуляции с содержимым сайта «выше», после того как мы находимся уже совершенно в другой части исполнения страницы и не имеем возможности перепроектировать последовательность под конкретный случай. 

В своём решении я использую событие OnEndBufferContent (вызывается при выводе буфферизированного контента), у него имеется один аргумент $content – тот самый буфферизированный контент, который мы будем править. 

Идея заключается в том, чтобы найти подключенный закешированный (объединённый и минифицированный) файл стилей, получить его содержимое, вырезать его подключение из кода и вставить содержимое в теге <style> в head. Маркер для вставки в виде пустого блока стилей, на случай если что-то пойдёт не так, страница останется целой и валидной: 

<st yle type="text/css"></style>

Метод будет работает стабильно всегда, так как получает имя сгенерированного битриксом файла через специальный api-метод, ищет его в контенте и ещё раз проверяет файл по имени (содержит template_), проверяет его существование на сервере и если он не пустой, вставляет в документ и очищает контент от link’a только вставленного файла. 

Результат можно посмотреть здесь, не обещаю, что будет всегда включено :)
Ещё, кстати, убираю css-комментарии и лишние переносы, это просто по фану. Не рекомендую сильно заморачиваться, также не рекомендую заморачиваться по поводу вырезания пробелов или ещё какой-то манипуляции с контентом, так как не стоит забывать, что html-результат сжимается gzip’ом, по хорошему конечно :) Но когда вы дойдёте до применения вставки стилей в head, то всё остальное по умолчанию у вас будет по феншую.

/*
 * Убрать комментарии из стилей и лишние переносы
 * можно применить как ко всему контенту, так и только к файлам стилей, но лучше вообще не заморачиваться :)
 */
$arReplace = array(
    "/\\/\\*(.*)\\*\\//" => "",
    "/\n+/"              => "\n",
);
$content = preg_replace(array_flip($arReplace), $arReplace, $content);


Метод протестирован с объединёнными файлами стилей и без, со сжатыми, минифицированными и без. Даже если вдруг произойдёт вставка не того файла, то только его подключение и будет вырезано, но такого замечено не было. Ещё по поводу времени исполнения, решайте сами, что для вас быстро или медленно, смотрите как это влияет на общую скорость отработки страницы в вашем случае. У меня это ~0,0009, но и контента на приведённом в примере сайте очень мало. 
Также не стоит забывать, что подключенные внешние файлы стилей будут кешироваться браузером и такой прямой подход как здесь увеличит объём страницы и уменьшит кол-во кешируемых данных, так что подходит не везде. Если у вас не много стилей, маленький или среднего размера проект, то скорее всего вы получите прирост. На крупных проектах метод не стоит использовать или стоит дорабатывать. 

Ремарка про шрифты,

var resource = document.createElement('link');
resource.setAttribute("rel", "stylesheet");
resource.setAttribute("href","https://fonts.googleapis.com/css?family=Open+Sans+Condensed:700|Open+Sans:400,600&subset=cyrillic,cyrillic-ext");
var head = document.getElementsByTagName('head')[0];
head.appendChild(resource);

Код асинхронно подгружает файлы шрифтов, подключаемых с fonts.googleapis.com. Но это создаёт одну неприятность. После загрузки шрифта, если в этот момент сайт уже отрендерин, страница заметно моргнёт. Проблему наблюдал в chrome, решение искать не стал и от идеи отказался, мигание ушло. Если вас ваше руководство/клиента это устроит, используйте, гугл прибавит вам пару «попугаев» к тесту. Если кому известно в чём причина и как исправить, пишите в комментариях. Наличие или отсутствие data-skip-moving на результат не влияет. При повторном открытие стр. проблемы нет, так как файл шрифтов уже в кеше браузера. 

UPD: рабочий код на github