Вставка CSS в <style> через маркеры и data-атрибуты
Всем добра! Пишу впервые статью, и повод для неё оказался вполне практичным: нужно было повысить показатель First Paint на одном из проектов. В процессе оптимизации стало понятно, что стандартное подключение CSS через
Становится узким горлышком — оно замедляет отрисовку и задерживает появление контента на экране.
Чтобы этого избежать, было решено встроить критически важные стили прямо в HTML, используя тег style.
Так браузер сразу видит нужные правила и начинает отрисовку без лишних пауз.
Что делает плагин?
Создаем плагин на событие OnWebPagePrerender
Конфигурация
Возможные способы задания:
Как работает?
1. Обработка события OnWebPagePrerender
2. Функция getLatestFile()
Для кейсов с MinifyX или похожими — ищем последний изменённый файл в папке по маске.
3. Способы вставки:
<link rel="stylesheet">
Становится узким горлышком — оно замедляет отрисовку и задерживает появление контента на экране.
Чтобы этого избежать, было решено встроить критически важные стили прямо в HTML, используя тег style.
Так браузер сразу видит нужные правила и начинает отрисовку без лишних пауз.
- Читает заранее определённый список CSS-файлов.
- Поддерживает: Явные пути к файлам, поиск последних файлов по маске (например, styles_*.css в MinifyX).
- Вставляет 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. Способы вставки:
- По data атрибуту
<style data-значение="ключ">
- По маркеру
<!-- inject:main -->
- В конец файла, если ни маркера, ни 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>
Комментарии: 6
Подскажите, на практике у реального пользователя на сколько уменьшилось время загрузки? Можно в процентах. Спасибо
P.S. А если JS файлы так же сделать будет какой-то толк?
P.S. А если JS файлы так же сделать будет какой-то толк?
На счет js файлов думаю что можно по такому же принципу сделать, но целесообразность этого не подскажу, у себя на проекте использую ES6 Modules загружаю только нужные скрипты в зависимости от секций + всякие фишки с использованием IntersectionObserver
По скорости прироста в процентах не подскажу, была задача повысить показатель всеми любимого google page speed и результат до использования инлайн стилей прыгал от 93-95 балов, при подключение основных файлов удалось добиться долгожданной сотки
По скорости прироста в процентах не подскажу, была задача повысить показатель всеми любимого google page speed и результат до использования инлайн стилей прыгал от 93-95 балов, при подключение основных файлов удалось добиться долгожданной сотки
Наверно было бы круто если бы можно было первый экран страницы обрамить маркером, например
И сделать проверку всех тегов и классов которые есть в этой области в html коде и чтобы плагин подключил в head из основного файла стилей, в все стили для всех тегов и классов которые ему встретились. А все остальные подключения стилей можно было бы разместить перед . Тогда бы у пользователя который заходит первый раз первый экран загрузился бы максимально быстро, а остальные стили уже чут позже.
И сделать проверку всех тегов и классов которые есть в этой области в html коде и чтобы плагин подключил в head из основного файла стилей, в все стили для всех тегов и классов которые ему встретились. А все остальные подключения стилей можно было бы разместить перед . Тогда бы у пользователя который заходит первый раз первый экран загрузился бы максимально быстро, а остальные стили уже чут позже.
Не совсем понимаю что имеете ввиду, но у себя я делаю так
Использую migx в админке выглядит вот так
и на каждую секцию генерирую свой css
Далее в head прохожусь только по текущим секциям страницы собираю объект path, который в свою очередь передаю в MinifyX
Для страниц где не используется migx просто добавляю по условию на нужные страницы
В общем суть я думаю ясна
Использую 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}
В общем суть я думаю ясна
А примерно понял что имели ввиду, можно сделать как то так
Обрамляем нужные элементы маркерами
Далее парсим всем классы, индификаторы, теги. Ищем их в файле стилей и формируем.
Но есть конечно же нюансы если к примеру используется какие либо js библиотеки по типу того же swiper slider которые после рендора внедряют свои классы элементам то это не сработает, в общем нужно обмозговать это дело, возможно что то и выкачу позже.
<!-- 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 которые после рендора внедряют свои классы элементам то это не сработает, в общем нужно обмозговать это дело, возможно что то и выкачу позже.
Да. На счет js не подумал
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.