Оптимизация CSS путем удаления неиспользуемых медиа-запросов

Оптимизация CSS путем удаления неиспользуемых медиа-запросов

От автора: поскольку производительность важна как для пользователей, так и для SEO, мы в Zoover.nl предпринимаем некоторые шаги, чтобы улучшить эти показатели. Это первая часть серии, в которой мы рассмотрим некоторые улучшения, которые мы выполняем.

Один из (поддерживаемых) показателей, которые мы пытаемся улучшить в этом триместре — это количество CSS, которое отправляется в браузер. Оптимизация CSS абсолютно важна для (воспринимаемой) производительности сайта: любой рендеринг задерживается до тех пор, пока ваши таблицы стилей не будут полностью загружены. Мы определили для себя бюджет в размере 50 килобайт несжатого CSS: не совсем совпадает с тем же лимитом, который AMP устанавливает для таблицы стилей.

Теперь, учитывая вашу ситуацию, это может быть либо очень амбициозная цель, либо то, чего достичь довольно просто. В нашем случае — это первое. И вот почему:

Мы используем CSS-модули, которые не оптимизированы для повторного использования;

У нас не на таком высоком уровне дисциплина системы проектирования, поэтому есть куча небольших вариаций, казалось бы, похожих компонентов и стилей;

На наших страницах много контента;

Мы постоянно проводим A / B тесты, что добавляет на страницу больше наворотов;

Мы должны поддерживать множество устройств и браузеров.

Чтобы уменьшить наш CSS, мы уже предприняли несколько действий.

Разделение кода CSS с помощью mini-extract-css-plugin

Оптимизация и минимизация CSS с помощью optimize-css-assets-webpack-plugin

Использование списка браузеров с помощью autoprefixer для устранения ненужных вендорных префиксов

К сожалению, этого недостаточно, чтобы вписываться в лимит 50 килобайт для некоторых наших страниц. Есть некоторые вещи, которые мы можем сделать в долгосрочной перспективе: оптимизировать систему дизайна под повторное использование, а также бороться с раздуванием функций. Однако для этого потребуются значительные инвестиции и следовательно согласования с заинтересованными сторонами. В качестве краткосрочного решения мы решили изучить возможность генерации различных вариантов наших таблиц стилей с использованием только стилей, применяемых для текущего устройства и окна просмотра. Это означает удаление любых стилей внутри неиспользуемых медиа-запросов.

Изучение вариантов

Прежде всего: элемент ссылки имеет атрибут media, не так ли? Вы можете использовать этот атрибут, чтобы указать браузеру применять только данную таблицу стилей, когда окно просмотра соответствует указанному медиа-запросу. Отлично, мы, вероятно, можем использовать это, чтобы загружать только таблицы стилей, которые используется.

Нет. Неправильно. Браузер по-прежнему будет загружать таблицы стилей с нерелевантным медиа-запросом, хотя он и не планирует их использовать. Вот тест Скотта Джеля, который демонстрирует это поведение: http://scottjehl.github.io/CSS-Download-Tests/

Итак, что еще мы можем использовать? Ну, данные. В Zoover мы используем классы экрана: экстра малый, малый, средний и большой. Грубо говоря, первые два относятся к телефонам, третий к планшетам и последний к компьютерам. Это оставляет возможность открыть заголовок User-Agent, чтобы определить, какое устройство запрашивает эту страницу, и затем загружать таблицу стилей, которая не содержит медиа-запросов, применяемых к классам экрана, не относящимся к этому устройству. Однако, поскольку существуют устройства с различными форматами и размерами, нет никакой гарантии, что у телефона никогда не будет «среднего» окна просмотра, или на планшете никогда не будет «малого» окна просмотра. Это подводит нас к шагу 1:

1. Получение данных

В качестве первого шага нам нужно было понять, насколько близко типы устройств соответствуют ожидаемому классу экрана. Поскольку мы уже отслеживаем загрузку страниц в браузере, нам было легко добавить некоторые метаданные в показатели по типу устройства, размерам окна просмотра и классу экрана. Затем мы можем увидеть распределение классов экранов для данного типа устройства. Вот визуализация классов экранов для компьютеров от Kibana:

Вы можете видеть, что почти все относится к классу экрана lg. Около 2% составляют md, а xs и sm — менее 1%. Это означает, что нам не нужны медиа-запросы для последних классов экрана в файле CSS. Аналогично, для планшета мы можем отказаться от xs, а для мобильных устройств мы можем удалить как lg, так и (что удивительно) md.

2. Препроцессинг вывода CSS Webpack

Теперь нам нужно каким-то образом подключиться к процессу компиляции Webpack. Казалось бы, очевидный способ сделать это — написать плагин Webpack. Плагин Webpack позволяет использовать определенные этапы процесса. Фаза, которая нам нужна — additionalAssets. Может быть есть более подходящая фаза, но это … работает. Уловка заключается в том, что нам нужно запускать после запуска mini-css-extract-plugin, но перед плагином optimize-css-assets-webpack, поскольку мы хотим передать собственный сгенерированный CSS в процесс минимизации, который у нас уже есть. Вот как примерно выглядит плагин:

const RawSource = require('webpack-sources').RawSource; class SplitCssByMediaPlugin { apply(compiler) { // wait until compilation becomes available compiler.hooks.compilation.tap( 'SplitByCssMediaPlugin', (compilation) => { // hook into additionalAssets phase compilation.hooks.additionalAssets.tap( 'SplitByCssMediaPlugin', () => { // get all about-to-be-emitted css files const cssFiles = Object.keys(compilation.assets) .filter(key => key.endsWith('.css')); cssFiles.forEach((filename) => { const rawSource = compilation.assets[filename]; // extract CSS string const css = rawSource.source(); // create device-type specific css const deviceSpecificCss = '..'; // add output back to compilation compilation.assets[name] = new RawSource(deviceSpecificCss); }); } ) } ); }
} module.exports = SplitCssByMediaPlugin;

Довольно просто! Теперь мы можем отправить весь CSS в наш маленький инструмент, который будет генерировать еще более эффективный CSS.

3. Удаление неиспользуемых медиа-запросов

Теперь, когда у нас есть CSS, нам нужно немного его обработать. Нам нужна функция, которая принимает именованные диапазоны и возвращает CSS для каждого диапазона, который не содержит неприменяемых медиа-запросов. Идеальным инструментом для такого процессора будет PostCSS, который в принципе является Babel для CSS. На сегодняшний день, конечно, кто-то уже написал что-то, что делает именно то, что нам нужно, но, так как делать что-то с PostCSS очень легко, и ради собственного эго мы просто собираемся написать собственный код:

// @flow import postcss from 'postcss'; import mediaQuery from 'css-mediaquery'; const HEIGHT = 800; /* контрольная точка выглядит так: { phone: { to: 480 }, tablet: { from: 320, to: 768 } } */ export default (css, breakpoints) => { // парсинг входного CSS const root = postcss.parse(css, { from: undefined }); const result = {}; Object.keys(breakpoints) .forEach((breakpoint) => { // клонируем дерево, чтобы мы могли безопасно изменять копию const clone = root.clone(); const config = breakpoints[breakpoint]; // перебираем все медма-правила clone.walkAtRules('media', (atRule) => { // проверяем соответствие верхнему или нижнему пределу // the current media query const matches = mediaQuery.match(atRule.params, { type: 'screen', // 0 не соответствует ничему width: config.from || 0.01, // на данный момент мы не поддерживаем вертикальные медиа-запросы, // поэтому мы просто используем среднюю высоту height: HEIGHT, }) || mediaQuery.match(atRule.params, { type: 'screen', width: config.to || Number.MAX_SAFE_INTEGER, height: HEIGHT, }); // и, если это так, удаляем это if (!matches) { atRule.remove(); } }); result[breakpoint] = clone; }); return result; };

Вот и все. Мы использовали css-mediaquery для парсинга и оценки медиа-запросов в Node (в браузере вы можете использовать window.matchMedia). Мы также следим за соблюдением порядка ввода CSS-файла. Затем мы можем вернуть этот вывод обратно в плагин Webpack, который мы написали ранее, и весь наш CSS для конкретного типа устройства будет записан на диск.

4. Использование определения типа устройства для обслуживания файлов CSS

Теперь, в качестве последнего шага, нам нужно обслуживать соответствующий CSS-файл в браузере. На этом этапе мы уже создали файлы CSS для конкретного устройства. Теперь, когда приходит запрос, мы можем использовать заголовок User-Agent, чтобы определить, какое устройство мы обслуживаем. Затем мы используем эту информацию для обслуживания обрезанного CSS-файла для конкретного типа устройства, который мы создали на предыдущем шаге:

import MobileDetect from 'mobile-detect'; export default ( userAgent ) => { // определяем тип устройства const md = new MobileDetect(userAgent); let deviceType = 'desktop'; if (md.phone()) { deviceType = 'phone'; } else if (md.tablet()) { deviceType = 'tablet'; } // ссылаемся на css для конкретного типа устройства const href = `styles.${deviceType}.css`; return `<link rel="stylesheet" href=${href} // заставляем Webpack думать, что таблица стилей уже загружена data-href="styles.css" />`; };

Здесь есть одна странная вещь: мы устанавливаем атрибут data-href для элемента ссылки. Это связано с тем, что mini-extract-css-plugin включает небольшой код во время выполнения, который отвечает за загрузку таблиц стилей, когда браузер перемещает ваше приложение. Эта среда выполнения должна проверить, была ли загружена таблица стилей. Способ, которым она это делает — проверяет атрибут href уже существующих таблиц стилей. К счастью для нас, она возвращается к data-href, а это значит, что мы можем обмануть Webpack, заставив думать, что таблица стилей, которую он ищет, уже загружена. Если мы этого не сделаем, среда выполнения Webpack немедленно добавит в документ исходную таблицу стилей, сводя на нет любые улучшения, которые мы надеялись получить от этого изменения. Вот как выглядит версия для компьютеров:

Снимок экрана сетевой консоли, показывающий, что загружены только соответствующие файлы CSS.

5. Оценка

Наши CSS-файлы стали примерно на 10% меньше. Вот результат одного из наших тестов производительности (где мы сравниваем ветки функций с нашей главной ветвью):

Снимок экрана наших тестов производительности, показывающий, что размер критического CSS уменьшился примерно на 12%.

В глобальном масштабе 10% — это довольно немного, и во многих случаях оно этого не стоит. Это будет не самая большая победа, которую мы можем одержать. Тем не менее, это дает нам возможность дышать немного свободнее, пока мы не приступим к более масштабным структурными решениями. Было интересно писать об этом, и фактически написание этой статьи заняло больше времени, чем сама реализация. Хотя это может быть началом моей деятельности, как автора.

Автор: Dario Gieselaar

Источник: https://medium.com/

Редакция: Команда webformyself.