Вставка CSS в <style> через маркеры и data-атрибуты

Всем добра! Пишу впервые статью, и повод для неё оказался вполне практичным: нужно было повысить показатель First Paint на одном из проектов. В процессе оптимизации стало понятно, что стандартное подключение CSS через

<link rel="stylesheet">

Становится узким горлышком — оно замедляет отрисовку и задерживает появление контента на экране.
Чтобы этого избежать, было решено встроить критически важные стили прямо в HTML, используя тег style.
Так браузер сразу видит нужные правила и начинает отрисовку без лишних пауз.

Что делает плагин?

  1. Читает заранее определённый список CSS-файлов.
  2. Поддерживает: Явные пути к файлам, поиск последних файлов по маске (например, styles_*.css в MinifyX).
  3. Вставляет CSS в документ по одному из трёх способов
    • По специальным маркерам в шаблоне:
      <!-- inject:ключ -->
    • В уже существующие style-теги с data-* атрибутами>.
    • В конец секции head — если ни маркер, ни подходящий style не найден.

Создаем плагин на событие OnWebPagePrerender

<?php
if ($modx->event->name !== 'OnWebPagePrerender') {
    return;
}

// Функция поиска последнего файла по маске
function getLatestFile($directory, $pattern = '*') {
    $files = glob(rtrim($directory, '/') . '/' . $pattern);
    if (empty($files)) {
        return false;
    }

    usort($files, function($a, $b) {
        return filemtime($b) - filemtime($a);
    });

    return $files[0];
}

// Конфигурация файлов и мест вставки
$cssFiles = [
  'header' => 'assets/components/project/app/css/layout/header.min.css',
  'main' => [
    'dir'     => 'assets/minifyx/css/', // ищем в нужной директории
    'pattern' => 'styles_*.css', // по маске
  ],
  'footer' => 'assets/components/project/app/css/layout/footer.min.css',
  'light' => [
    'path' => 'assets/components/project/app/css/light.min.css',
    'data' => 'theme' //  <style data-theme="light">
  ],
  'dark' => [
    'path' => 'assets/components/project/app/css/dark.min.css',
    'data' => 'theme' //  <style data-theme="dark">
  ],
];

$output = &$modx->resource->_output;

foreach ($cssFiles as $key => $config) {
    $isDataAttr = false;
    $dataAttrName = '';
    $filePath = '';

    // Определяем путь к файлу
    if (is_array($config)) {
        if (!empty($config['dir']) && !empty($config['pattern'])) {
            $filePath = getLatestFile(MODX_BASE_PATH . $config['dir'], $config['pattern']);
        } elseif (!empty($config['path'])) {
            $filePath = MODX_BASE_PATH . $config['path'];
        }

        if (!empty($config['data'])) {
            $isDataAttr = true;
            $dataAttrName = $config['data'];
        }
    } else {
        $filePath = MODX_BASE_PATH . $config;
    }

    if (empty($filePath) || !file_exists($filePath) || !is_readable($filePath)) {
        $modx->log(modX::LOG_LEVEL_WARN, "[InlineCSSInjector] Файл не найден или недоступен: {$filePath}");
        continue;
    }

    // Загружаем CSS
    $cssContent = file_get_contents($filePath);
    $cssContent = preg_replace('#</?style.*?>#is', '', $cssContent);
    $styleContent = "\n/* === {$key} === */\n" . trim($cssContent) . "\n";

    // === 1. Вставка по <style data-имя="значение">
    if ($isDataAttr) {
        $attrName = preg_quote($dataAttrName, '#');
        $attrValue = preg_quote($key, '#');

        $pattern = '#<style\b([^>]*?\bdata-' . $attrName . '=["\']' . $attrValue . '["\'][^>]*)>(.*?)</style>#is';
        if (preg_match($pattern, $output, $matches)) {
            $newContent = $matches[2] . "\n" . $styleContent;
            $newTag = "<style {$matches[1]}>" . $newContent . "</style>";
            $output = preg_replace($pattern, $newTag, $output, 1);
            continue;
        }
    }

    // === 2. Вставка по <!-- inject:key -->
    $marker = '<!-- inject:' . $key . ' -->';
    if (strpos($output, $marker) !== false) {
        $output = str_replace($marker, "<style>\n" . $styleContent . "</style>", $output);
        continue;
    }

    // === 3. Вставка в конец <head>
    $styleTag = "<style>\n" . $styleContent . "</style>\n";
    $output = str_ireplace('</head>', $styleTag . '</head>', $output);
}

Конфигурация

$cssFiles = [
  'header' => 'assets/components/project/app/css/layout/header.min.css',
  'main' => [
    'dir'     => 'assets/minifyx/css/', // ищем в нужной директории
    'pattern' => 'styles_*.css', // по маске
  ],
  'footer' => 'assets/components/project/app/css/layout/footer.min.css',
  'light' => [
    'path' => 'assets/components/project/app/css/light.min.css',
    'data' => 'theme' //  <style data-theme="light">
  ],
  'dark' => [
    'path' => 'assets/components/project/app/css/dark.min.css',
    'data' => 'theme' //  <style data-theme="dark">
  ],
];

Возможные способы задания:
  • Строка — прямой путь к CSS.
  • Массив:
    • 'path' — прямой путь.
    • 'dir' + 'pattern' — для поиска самого нового файла по шаблону.
    • 'data' => 'значениеАтрибута' — указывает, что нужно вставить в <style data-значение=«ключ»>.

Как работает?

1. Обработка события OnWebPagePrerender

2. Функция getLatestFile()
Для кейсов с MinifyX или похожими — ищем последний изменённый файл в папке по маске.

3. Способы вставки:
  1. По data атрибуту
    <style data-значение="ключ">
  2. По маркеру
    <!-- inject:main -->
  3. В конец файла, если ни маркера, ни data-атрибута нет — CSS попадёт в самый конец тега head.
Пример шаблона

<head>
<!-- inject:header -->
<!-- inject:main -->
<!-- inject:footer -->

<style data-theme="light"></style>
<style data-theme="dark"></style>
<style data-block="faq"></style>

<!-- остальные блоки -->
</head>
Дмитрий Середюк
02 июня 2025, 13:27
modx.pro
3
380
+5

Комментарии: 6

Ivan
04 июня 2025, 00:38
0
Подскажите, на практике у реального пользователя на сколько уменьшилось время загрузки? Можно в процентах. Спасибо

P.S. А если JS файлы так же сделать будет какой-то толк?
    Дмитрий Середюк
    04 июня 2025, 03:19
    0
    На счет js файлов думаю что можно по такому же принципу сделать, но целесообразность этого не подскажу, у себя на проекте использую ES6 Modules загружаю только нужные скрипты в зависимости от секций + всякие фишки с использованием IntersectionObserver

    По скорости прироста в процентах не подскажу, была задача повысить показатель всеми любимого google page speed и результат до использования инлайн стилей прыгал от 93-95 балов, при подключение основных файлов удалось добиться долгожданной сотки
    Ivan
    04 июня 2025, 00:48
    0
    Наверно было бы круто если бы можно было первый экран страницы обрамить маркером, например

    И сделать проверку всех тегов и классов которые есть в этой области в html коде и чтобы плагин подключил в head из основного файла стилей, в все стили для всех тегов и классов которые ему встретились. А все остальные подключения стилей можно было бы разместить перед . Тогда бы у пользователя который заходит первый раз первый экран загрузился бы максимально быстро, а остальные стили уже чут позже.
      Дмитрий Середюк
      04 июня 2025, 03:30
      0
      Не совсем понимаю что имеете ввиду, но у себя я делаю так
      Использую migx в админке выглядит вот так
      и на каждую секцию генерирую свой css
      Далее в head прохожусь только по текущим секциям страницы собираю объект path, который в свою очередь передаю в MinifyX

      {set $rows = json_decode($_modx->resource.id | resource : 'multiMigx', true)}
      {if $rows}
          {set $arrDouble=[]}
          {set $path=[]}
          {foreach $rows as $row}
            {if $row.MIGX_formname | in : $arrDouble}{continue}{/if}
            {set $arrDouble[] = $row.MIGX_formname}
            {if $row.MIGX_formname != 'default' && $row.MIGX_formname != 'code'}
              {set $path[] = ('project_path' | config)~'css/modules/'~$row.MIGX_formname~'.min.css'}
            {/if}
          {/foreach}
      {/if}
      
      {'!MinifyX' | snippet : [
          'minifyCss' => 1,
          'cssSources' => ($path | join : ','),
      ]}

      Для страниц где не используется migx просто добавляю по условию на нужные страницы

      {if $_modx->resource.id === 245}
          {set $path=[
            ('project_path' | config)~'css/modules/cart.min.css',
            ('project_path' | config)~'css/modules/order.min.css',
          ]}
      {/if}

      В общем суть я думаю ясна
        Дмитрий Середюк
        04 июня 2025, 03:57
        +1
        А примерно понял что имели ввиду, можно сделать как то так

        <!-- above-the-fold:start -->
            <header class="site-header">
                <div class="logo-wrapper">
                    <img src="logo.svg" alt="Логотип" class="logo">
                </div>
                <nav class="main-nav">
                    <a href="#" class="nav-link active">Главная</a>
                    <a href="#" class="nav-link">Контакты</a>
                </nav>
            </header>
        
            <section class="hero-slider js-slider">
                <div class="slide">Слайд 1</div>
                <div class="slide">Слайд 2</div>
            </section>
        <!-- above-the-fold:end -->

        Обрамляем нужные элементы маркерами

        Далее парсим всем классы, индификаторы, теги. Ищем их в файле стилей и формируем.
        Но есть конечно же нюансы если к примеру используется какие либо js библиотеки по типу того же swiper slider которые после рендора внедряют свои классы элементам то это не сработает, в общем нужно обмозговать это дело, возможно что то и выкачу позже.
          Ivan
          04 июня 2025, 12:13
          0
          Да. На счет js не подумал
        Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
        6