Обработка неиспользуемого CSS в Sass для повышения производительности

Опубликовано: 2022-03-10
Краткое резюме ↬ Знаете ли вы, как неиспользуемый CSS влияет на производительность? Спойлер: это много! В этой статье мы рассмотрим ориентированное на Sass решение для работы с неиспользуемым CSS, избегая необходимости в сложных зависимостях Node.js, включая безголовые браузеры и эмуляцию DOM.

В современной фронтенд-разработке разработчики должны стремиться к тому, чтобы CSS был масштабируемым и удобным в сопровождении. В противном случае они рискуют потерять контроль над особенностями, такими как каскадность и специфичность селектора, по мере роста кодовой базы и увеличения числа разработчиков.

Одним из способов достижения этого является использование таких методологий, как объектно-ориентированный CSS (OOCSS), который вместо того, чтобы организовывать CSS вокруг контекста страницы, поощряет отделение структуры (системы сетки, интервалы, ширина и т. д.) от оформления (шрифты, марка, цвет и др.).

Итак, имена классов CSS, такие как:

  • .blog-right-column
  • .latest_topics_list
  • .job-vacancy-ad

Заменяются более многоразовыми альтернативами, которые применяют те же стили CSS, но не привязаны к какому-либо конкретному контексту:

  • .col-md-4
  • .list-group
  • .card

Этот подход обычно реализуется с помощью фреймворка Sass, такого как Bootstrap, Foundation, или все чаще с помощью специального фреймворка, который можно настроить так, чтобы он лучше соответствовал проекту.

Еще после прыжка! Продолжить чтение ниже ↓

Итак, теперь мы используем классы CSS, отобранные из структуры шаблонов, компонентов пользовательского интерфейса и служебных классов. В приведенном ниже примере показана обычная сеточная система, построенная с использованием Bootstrap, которая складывается вертикально, а затем, как только достигается точка останова md, переключается на макет из 3 столбцов.

 <div class="container"> <div class="row"> <div class="col-12 col-md-4">Column 1</div> <div class="col-12 col-md-4">Column 2</div> <div class="col-12 col-md-4">Column 3</div> </div> </div>

Для создания этого шаблона здесь используются программно сгенерированные классы, такие как .col-12 и .col-md-4 . Но как насчет .col-1 через .col-11 , .col-lg-4 , .col-md-6 или .col-sm-12 ? Это все примеры классов, которые будут включены в скомпилированную таблицу стилей CSS, загружены и проанализированы браузером, несмотря на то, что они не используются.

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

Измерение влияния неиспользуемых классов CSS

Хотя я обожаю Шеффилд Юнайтед, могучие клинки, CSS их веб-сайта упакован в один мини-файл размером 568 КБ, который достигает 105 КБ даже в сжатом виде. Кажется, это много.

Это веб-сайт «Шеффилд Юнайтед», моей местной футбольной команды (это футбол для вас, в колониях). (Большой превью)

Посмотрим, какая часть этого CSS на самом деле используется на их главной странице? Быстрый поиск в Google показывает множество онлайн-инструментов, пригодных для работы, но я предпочитаю использовать инструмент охвата в Chrome, который можно запустить прямо из Chrome DevTools. Давайте попробуем.

Самый быстрый способ получить доступ к инструменту покрытия в инструментах разработчика — использовать сочетание клавиш Control+Shift+P или Command+Shift+P (Mac), чтобы открыть меню команд. В нем введите coverage и выберите опцию «Показать покрытие». (Большой превью)

Результаты показывают, что только 30 КБ CSS из таблицы стилей размером 568 КБ используются главной страницей, а остальные 538 КБ относятся к стилям, необходимым для остальной части веб-сайта. Это означает, что колоссальные 94,8% CSS не используются.

Вы можете увидеть такие тайминги для любого актива в Chrome в Инструментах разработчика через сеть -> Нажмите на свой актив -> вкладка «Время». (Большой превью)

CSS является частью критического пути рендеринга веб-страницы, который включает в себя все различные шаги, которые браузер должен выполнить, прежде чем он сможет начать рендеринг страницы. Это делает CSS активом, блокирующим рендеринг.

Имея это в виду, при загрузке веб-сайта «Шеффилд Юнайтед» с использованием хорошего 3G-соединения требуется целых 1,15 с, прежде чем загрузится CSS и начнется рендеринг страницы. Это проблема.

Google также признал это. При проведении аудита Lighthouse, онлайн или через браузер, выделяется любая потенциальная экономия времени загрузки и размера файла, которая может быть достигнута за счет удаления неиспользуемого CSS.

В Chrome (и Chromium Edge) вы можете настроить аудит Google Lighthouse, щелкнув вкладку «Аудит» в инструментах разработчика. (Большой превью)

Существующие решения

Цель состоит в том, чтобы определить, какие классы CSS не требуются, и удалить их из таблицы стилей. Доступны существующие решения, которые пытаются автоматизировать этот процесс. Обычно их можно использовать с помощью сценария сборки Node.js или с помощью средств запуска задач, таких как Gulp. Это включает:

  • UNCSS
  • ОчиститьCSS
  • ОчиститьCSS

Обычно они работают аналогичным образом:

  1. На быке доступ к веб-сайту осуществляется через безголовый браузер (например, puppeteer) или эмуляцию DOM (например, jsdom).
  2. На основе HTML-элементов страницы определяется любой неиспользуемый CSS.
  3. Это удаляется из таблицы стилей, оставляя только то, что необходимо.

Несмотря на то, что эти автоматизированные инструменты вполне работоспособны, и я успешно использовал многие из них в ряде коммерческих проектов, я столкнулся с несколькими недостатками, которыми стоит поделиться:

  • Если имена классов содержат специальные символы, такие как '@' или '/', они могут быть не распознаны без написания специального кода. Я использую BEM-IT Гарри Робертса, который включает в себя структурирование имен классов с адаптивными суффиксами, такими как: u-width-6/12@lg , поэтому я столкнулся с этой проблемой раньше.
  • Если веб-сайт использует автоматическое развертывание, это может замедлить процесс сборки, особенно если у вас много страниц и много CSS.
  • Знания об этих инструментах должны быть распространены среди всей команды, иначе может возникнуть путаница и разочарование, когда CSS таинственным образом отсутствует в рабочих таблицах стилей.
  • Если на вашем веб-сайте запущено много сторонних скриптов, иногда при открытии в автономном браузере они не работают должным образом и могут вызвать ошибки в процессе фильтрации. Поэтому, как правило, вам нужно написать собственный код, чтобы исключить любые сторонние сценарии при обнаружении безголового браузера, что в зависимости от вашей настройки может быть сложным.
  • Как правило, такие инструменты сложны и вводят много дополнительных зависимостей в процесс сборки. Как и в случае со всеми сторонними зависимостями, это означает использование чужого кода.

Имея в виду эти моменты, я задал себе вопрос:

Можно ли, используя только Sass, лучше обрабатывать Sass, который мы компилируем, чтобы можно было исключить любой неиспользуемый CSS, не прибегая к простому грубому удалению исходных классов в Sass?

Спойлер: ответ — да. Вот что я придумал.

Sass-ориентированное решение

Решение должно обеспечивать быстрый и простой способ выбора того, что Sass следует скомпилировать, и в то же время быть достаточно простым, чтобы не усложнять процесс разработки и не мешать разработчикам использовать преимущества таких вещей, как программно сгенерированный CSS. классы.

Для начала есть репозиторий со скриптами сборки и несколькими образцами стилей, которые вы можете клонировать отсюда.

Совет: если вы застряли, вы всегда можете сослаться на завершенную версию в главной ветке.

cd в репозиторий, запустите npm install , а затем npm run build , чтобы скомпилировать любой Sass в CSS по мере необходимости. Это должно создать файл css размером 55 КБ в каталоге dist.

Если вы затем откроете /dist/index.html в своем веб-браузере, вы должны увидеть довольно стандартный компонент, который при щелчке расширяется, открывая некоторый контент. Вы также можете просмотреть это здесь, где будут применяться реальные сетевые условия, чтобы вы могли запускать свои собственные тесты.

Мы будем использовать этот расширяющий компонент пользовательского интерфейса в качестве объекта тестирования при разработке ориентированного на Sass решения для обработки неиспользуемого CSS. (Большой превью)

Фильтрация на уровне частей

В типичной настройке SCSS у вас, скорее всего, будет один файл манифеста (например main.scss в репозитории) или по одному на страницу (например, index.scss , products.scss , contact.scss ), где частичные фреймворки импортируются. Следуя принципам OOCSS, этот импорт может выглядеть примерно так:

Пример 1

 /* Undecorated design patterns */ @import 'objects/box'; @import 'objects/container'; @import 'objects/layout'; /* UI components */ @import 'components/button'; @import 'components/expander'; @import 'components/typography'; /* Highly specific helper classes */ @import 'utilities/alignments'; @import 'utilities/widths';

Если какой-либо из этих фрагментов не используется, то естественным способом фильтрации этого неиспользуемого CSS будет просто отключить импорт, что предотвратит его компиляцию.

Например, при использовании только компонента расширения манифест обычно будет выглядеть следующим образом:

Пример 2

 /* Undecorated design patterns */ // @import 'objects/box'; // @import 'objects/container'; // @import 'objects/layout'; /* UI components */ // @import 'components/button'; @import 'components/expander'; // @import 'components/typography'; /* Highly specific helper classes */ // @import 'utilities/alignments'; // @import 'utilities/widths';

Однако, согласно OOCSS, мы отделяем оформление от структуры, чтобы обеспечить максимальное повторное использование, поэтому возможно, что расширителю может потребоваться CSS от других объектов, компонентов или служебных классов для правильного отображения. Если разработчик не узнает об этих отношениях, изучив HTML, он может не знать, что нужно импортировать эти партиалы, поэтому не все необходимые классы будут скомпилированы.

В репозитории, если вы посмотрите на HTML-код расширителя в dist/index.html , похоже, это так. Он использует стили из блоков и объектов макета, компонент типографики, а также утилиты ширины и выравнивания.

dist/index.html

 <div class="c-expander"> <div class="o-box o-box--spacing-small c-expander__trigger c-expander__header" tabindex="0"> <div class="o-layout o-layout--fit u-flex-middle"> <div class="o-layout__item u-width-grow"> <h2 class="c-type-echo">Toggle Expander</h2> </div> <div class="o-layout__item u-width-shrink"> <div class="c-expander__header-icon"></div> </div> </div> </div> <div class="c-expander__content"> <div class="o-box o-box--spacing-small"> Lorum ipsum <p class="u-align-center"> <button class="c-expander__trigger c-button">Close</button> </p> </div> </div> </div>

Давайте решим эту проблему, ожидающую возникновения, сделав эти отношения официальными внутри самого Sass, чтобы после импорта компонента все зависимости также были импортированы автоматически. Таким образом, у разработчика больше нет дополнительных накладных расходов на проверку HTML, чтобы узнать, что еще им нужно импортировать.

Карта алгоритмического импорта

Чтобы эта система зависимостей работала, вместо того, чтобы просто комментировать операторы @import в файле манифеста, логика Sass должна будет определять, будут ли компилироваться партиалы или нет.

В src/scss/settings создайте новый партиал с именем _imports.scss , @import в settings/_core.scss , а затем создайте следующую карту SCSS:

src/scss/settings/_core.scss

 @import 'breakpoints'; @import 'spacing'; @import 'imports';

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container', 'layout' ), component: ( 'button', 'expander', 'typography' ), utility: ( 'alignments', 'widths' ) );

Эта карта будет иметь ту же роль, что и манифест импорта в примере 1.

Пример 4

 $imports: ( object: ( //'box', //'container', //'layout' ), component: ( //'button', 'expander', //'typography' ), utility: ( //'alignments', //'widths' ) );

Он должен вести себя так же, как стандартный набор @imports , в том смысле, что если некоторые частичные части закомментированы (как указано выше), тогда этот код не должен компилироваться при сборке.

Но поскольку мы хотим автоматически импортировать зависимости, мы также должны иметь возможность игнорировать эту карту при определенных обстоятельствах.

Рендер миксин

Давайте начнем добавлять логику Sass. Создайте _render.scss в src/scss/tools , а затем добавьте его @import в tools/_core.scss .

В файле создайте пустой миксин с именем render() .

src/scss/tools/_render.scss

 @mixin render() { }

В миксине нам нужно написать Sass, который делает следующее:

  • оказывать()
    «Эй, $imports , хорошая погода, не так ли? Скажите, у вас есть объект-контейнер на вашей карте?»
  • $импорт
    false
  • оказывать()
    «Жалко, похоже, тогда его не скомпилируют. Как насчет компонента кнопки?»
  • $импорт
    true
  • оказывать()
    "Хороший! Это кнопка компилируется тогда. Передай от меня привет жене».

В Sass это означает следующее:

src/scss/tools/_render.scss

 @mixin render($name, $layer) { @if(index(map-get($imports, $layer), $name)) { @content; } }

По сути, проверьте, включен ли партиал в переменную $imports imports, и если да, отобразите его с помощью директивы Sass @content , которая позволяет нам передать блок контента в миксин.

Мы бы использовали его так:

Пример 5

 @include render('button', 'component') { .c-button { // styles et al } // any other class declarations }

Прежде чем использовать этот миксин, мы можем внести в него небольшое улучшение. Имя слоя (объект, компонент, утилита и т. д.) — это то, что мы можем с уверенностью предсказать, поэтому у нас есть возможность немного упростить процесс.

Перед объявлением миксина рендеринга создайте переменную с именем $layer и удалите переменную с таким же именем из параметров миксина. Вот так:

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if(index(map-get($imports, $layer), $name)) { @content; } }

Теперь в _core.scss , где расположены объекты, компоненты и утилита @imports , переобъявите эти переменные со следующими значениями; представляющий тип импортируемых классов CSS.

src/scss/objects/_core.scss

 $layer: 'object'; @import 'box'; @import 'container'; @import 'layout';

src/scss/components/_core.scss

 $layer: 'component'; @import 'button'; @import 'expander'; @import 'typography';

src/scss/utilities/_core.scss

 $layer: 'utility'; @import 'alignments'; @import 'widths';

Таким образом, когда мы используем миксин render() , все, что нам нужно сделать, это объявить частичное имя.

Оберните миксин render() вокруг каждого объявления объекта, компонента и служебного класса, как показано ниже. Это даст вам одно использование миксина рендеринга для каждого партиала.

Например:

src/scss/objects/_layout.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

src/scss/components/_button.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

Примечание. Для utilities/_widths.scss обертывание функции render() вокруг всего партиала приведет к ошибке при компиляции, так как в Sass вы не можете вкладывать объявления миксинов в вызовы миксинов. Вместо этого просто оберните миксин render() вокруг вызовов create-widths() , как показано ниже:

 @include render('widths') { // GENERATE STANDARD WIDTHS //--------------------------------------------------------------------- // Example: .u-width-1/3 @include create-widths($utility-widths-sets); // GENERATE RESPONSIVE WIDTHS //--------------------------------------------------------------------- // Create responsive variants using settings.breakpoints // Changes width when breakpoint is hit // Example: .u-width-1/3@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include create-widths($utility-widths-sets, \@, #{$bp-name}); } } // End render }

При этом при сборке будут скомпилированы только партиалы, указанные в $imports .

Смешайте и сопоставьте компоненты, закомментированные в $imports , и запустите npm run build в терминале, чтобы попробовать.

Карта зависимостей

Теперь, когда мы программно импортируем партиалы, мы можем приступить к реализации логики зависимостей.

В src/scss/settings создайте новый партиал с именем _dependencies.scss , @import в settings/_core.scss , но убедитесь, что он стоит после _imports.scss . Затем в нем создайте следующую карту SCSS:

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( 'box', 'layout' ), component: ( 'button', 'typography' ), utility: ( 'alignments', 'widths' ) ) );

Здесь мы объявляем зависимости для компонента-расширителя, так как он требует правильную визуализацию стилей из других частей, как показано в dist/index.html.

Используя этот список, мы можем написать логику, которая будет означать, что эти зависимости всегда будут компилироваться вместе с их зависимыми компонентами, независимо от состояния переменной $imports .

Ниже $dependencies создайте миксин с именем dependency-setup() . Здесь мы выполним следующие действия:

1. Прокрутите карту зависимостей.

 @mixin dependency-setup() { @each $componentKey, $componentValue in $dependencies { } }

2. Если компонент можно найти в $imports , выполните цикл по его списку зависимостей.

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { } } } }

3. Если зависимости нет в $imports , добавьте ее.

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { @each $partKey, $partValue in $layerValue { @if not index(map-get($imports, $layerKey), $partKey) { $imports: map-merge($imports, ( $layerKey: append(map-get($imports, $layerKey), '#{$partKey}') )) !global; } } } } } }

Включение флага !global указывает Sass искать переменную $imports в глобальной области видимости, а не в локальной области видимости миксина.

4. Тогда остается только вызвать миксин.

 @mixin dependency-setup() { ... } @include dependency-setup();

Теперь у нас есть усовершенствованная система частичного импорта, в которой, если компонент импортирован, разработчику не нужно вручную импортировать каждый из его различных частичных зависимостей.

Настройте переменную $imports так, чтобы импортировался только компонент расширения, а затем запустите npm run build . Вы должны увидеть в скомпилированном CSS классы-расширители вместе со всеми его зависимостями.

Однако на самом деле это не привносит ничего нового в таблицу с точки зрения фильтрации неиспользуемого CSS, поскольку такое же количество Sass все еще импортируется, программно или нет. Давайте улучшим это.

Улучшенный импорт зависимостей

Компоненту может потребоваться только один класс из зависимости, так что затем продолжить и импортировать все классы этой зависимости просто приведет к тому же ненужному раздуванию, которого мы пытаемся избежать.

Мы можем усовершенствовать систему, чтобы обеспечить более точную фильтрацию по классам, чтобы убедиться, что компоненты компилируются только с теми классами зависимостей, которые им требуются.

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

Для имен классов, использующих установленное соглашение об именах, такое как БЭМ, обычно требуются как минимум именованные классы «Блок» и «Элемент», а «Модификаторы» обычно необязательны.

Примечание: служебные классы обычно не следуют маршруту БЭМ, поскольку они изолированы по своей природе из-за своей узкой направленности.

Например, взгляните на этот медиа-объект, который, вероятно, является самым известным примером объектно-ориентированного CSS:

 <div class="o-media o-media--spacing-small"> <div class="o-media__image"> <img src="url" alt="Image"> </div> <div class="o-media__text"> Oh! </div> </div>

Если компонент имеет этот набор в качестве зависимости, имеет смысл всегда компилировать .o-media , .o-media__image и .o-media__text , поскольку это минимальный объем CSS, необходимый для работы шаблона. Однако, .o-media--spacing-small является необязательным модификатором, его следует компилировать только в том случае, если мы явно указываем это, поскольку его использование может быть несогласованным во всех экземплярах медиа-объекта.

Мы изменим структуру карты $dependencies , чтобы позволить нам импортировать эти необязательные классы, а также включим способ импорта только блока и элемента, если модификаторы не требуются.

Для начала проверьте HTML-расширитель в dist/index.html и запишите все используемые классы зависимостей. Запишите их на карте $dependencies , как показано ниже:

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( box: ( 'o-box--spacing-small' ), layout: ( 'o-layout--fit' ) ), component: ( button: true, typography: ( 'c-type-echo', ) ), utility: ( alignments: ( 'u-flex-middle', 'u-align-center' ), widths: ( 'u-width-grow', 'u-width-shrink' ) ) ) );

Если установлено значение true, мы переведем это в «Только компилировать классы уровня блоков и элементов, без модификаторов!».

Следующий шаг включает в себя создание переменной белого списка для хранения этих классов и любых других (независимых) классов, которые мы хотим импортировать вручную. В /src/scss/settings/imports.scss после $imports imports создайте новый список Sass с именем $global-filter .

src/scss/settings/_imports.scss

 $global-filter: ();

Основная предпосылка $global-filter заключается в том, что любые классы, хранящиеся здесь, будут компилироваться при сборке, если партиал, которому они принадлежат, импортируется через $imports .

Эти имена классов могут быть добавлены программно, если они являются зависимостью компонента, или могут быть добавлены вручную при объявлении переменной, как в примере ниже:

Пример глобального фильтра

 $global-filter: ( 'o-box--spacing-regular@md', 'u-align-center', 'u-width-6/12@lg' );

Затем нам нужно добавить еще немного логики в миксин @dependency-setup , чтобы все классы, на которые есть ссылки в $dependencies , автоматически добавлялись в наш белый список $global-filter .

Под этим блоком:

src/scss/settings/_dependencies.scss

 @if not index(map-get($imports, $layerKey), $partKey) { }

... добавьте следующий фрагмент.

src/scss/settings/_dependencies.scss

 @each $class in $partValue { $global-filter: append($global-filter, '#{$class}', 'comma') !global; }

Это перебирает все классы зависимостей и добавляет их в белый список $global-filter .

На этом этапе, если вы добавите оператор @debug под миксин dependency-setup() , чтобы распечатать содержимое $global-filter в терминале:

 @debug $global-filter;

... вы должны увидеть что-то вроде этого при сборке:

 DEBUG: "o-box--spacing-small", "o-layout--fit", "c-box--rounded", "true", "true", "u-flex-middle", "u-align-center", "u-width-grow", "u-width-shrink"

Теперь у нас есть белый список классов, нам нужно применить его для всех различных частей объектов, компонентов и утилит.

Создайте новый партиал с именем _filter.scss в src/scss/tools и добавьте @import в файл _core.scss слоя инструментов.

В этом новом партиале мы создадим миксин с именем filter() . Мы будем использовать это для применения логики, которая означает, что классы будут скомпилированы, только если они включены в переменную $global-filter .

Начнем с простого: создайте миксин, который принимает единственный параметр — $class , которым управляет фильтр. Затем, если $class включен в белый список $global-filter , разрешите его компиляцию.

src/scss/tools/_filter.scss

 @mixin filter($class) { @if(index($global-filter, $class)) { @content; } }

В партиале мы бы обернули миксин вокруг необязательного класса, например так:

 @include filter('o-myobject--modifier') { .o-myobject--modifier { color: yellow; } }

Это означает, что класс .o-myobject--modifier будет скомпилирован только в том случае, если он включен в $global-filter , который можно установить либо напрямую, либо косвенно через то, что установлено в $dependencies .

Просмотрите репозиторий и примените миксин filter() ко всем необязательным классам модификаторов в слоях объектов и компонентов. При работе с компонентом типографики или уровнем утилит, поскольку каждый класс независим от следующего, имеет смысл сделать их все необязательными, чтобы мы могли просто включать классы по мере необходимости.

Вот несколько примеров:

src/scss/objects/_layout.scss

 @include filter('o-layout__item--fit-height') { .o-layout__item--fit-height { align-self: stretch; } }

src/scss/utilities/_alignments.scss

 // Changes alignment when breakpoint is hit // Example: .u-align-left@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include filter('u-align-left@#{$bp-name}') { .u-align-left\@#{$bp-name} { text-align: left !important; } } @include filter('u-align-center@#{$bp-name}') { .u-align-center\@#{$bp-name} { text-align: center !important; } } @include filter('u-align-right@#{$bp-name}') { .u-align-right\@#{$bp-name} { text-align: right !important; } } } }

Примечание. При добавлении имен классов с адаптивным суффиксом в миксин filter() вам не нужно экранировать символ «@» символом «\».

Во время этого процесса, применяя миксин filter() к партиалам, вы могли (или не могли) заметить несколько вещей.

Сгруппированные классы

Некоторые классы в кодовой базе сгруппированы вместе и используют одни и те же стили, например:

src/scss/objects/_box.scss

 .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; }

Поскольку фильтр принимает только один класс, он не учитывает возможность того, что один блок объявления стиля может относиться к более чем одному классу.

Чтобы учесть это, мы расширим миксин filter() , чтобы в дополнение к одному классу он мог принимать список аргументов Sass, содержащий множество классов. Вот так:

src/scss/objects/_box.scss

 @include filter('o-box--spacing-disable-left', 'o-box--spacing-horizontal') { .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; } }

Итак, нам нужно сообщить миксину filter() , что если какой-либо из этих классов находится в $global-filter , вам разрешено компилировать классы.

Это потребует дополнительной логики для проверки типа аргумента $class миксина, отвечая циклом, если передается список аргументов, чтобы проверить, находится ли каждый элемент в переменной $global-filter .

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

Затем нужно просто вернуться к следующим частям, чтобы правильно применить миксин filter() :

  • objects/_box.scss
  • objects/_layout.scss
  • utilities/_alignments.scss

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

  • Классы блоков и элементов, принадлежащие компоненту-расширителю, но не его модификатору.
  • Классы блоков и элементов, принадлежащие зависимостям расширителя.
  • Любые классы-модификаторы, принадлежащие зависимостям расширителя, которые явно объявлены в переменной $dependencies .

Теоретически, если вы решили, что хотите включить в скомпилированную таблицу стилей больше классов, таких как модификатор компонентов расширения, это просто вопрос добавления его в переменную $global-filter в точке объявления или в какой-либо другой точке. в кодовой базе (пока это до точки, где объявляется сам модификатор).

Включение всего

Итак, теперь у нас есть довольно полная система, которая позволяет вам импортировать объекты, компоненты и утилиты вплоть до отдельных классов внутри этих партиалов.

Во время разработки, по какой-либо причине, вы можете просто захотеть включить все за один раз. Чтобы учесть это, мы создадим новую переменную с именем $enable-all-classes , а затем добавим некоторую дополнительную логику, поэтому, если для нее установлено значение true, все компилируется независимо от состояния $imports imports и $global-filter переменные $global-filter .

Во-первых, объявите переменную в нашем основном файле манифеста:

src/scss/main.scss

 $enable-all-classes: false; @import 'settings/core'; @import 'tools/core'; @import 'generic/core'; @import 'elements/core'; @import 'objects/core'; @import 'components/core'; @import 'utilities/core';

Затем нам просто нужно внести несколько незначительных изменений в наши filter() и render() , чтобы добавить некоторую логику переопределения, когда для переменной $enable-all-classes установлено значение true.

Во-первых, миксин filter() . Перед любыми существующими проверками мы добавим оператор @if , чтобы увидеть, установлено ли для $enable-all-classes значение true, и если это так, визуализируем @content без вопросов.

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if($enable-all-classes) { @content; } @else if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

Далее в миксине render() нам просто нужно проверить, является ли переменная $enable-all-classes правдивой, и если да, то пропустить дальнейшие проверки.

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if($enable-all-classes or index(map-get($imports, $layer), $name)) { @content; } }

Так что теперь, если бы вы установили для переменной $enable-all-classes значение true и перестроили, каждый необязательный класс был бы скомпилирован, что сэкономило бы вам немало времени в процессе.

Сравнения

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

Чтобы сравнение было корректным, мы должны добавить объекты box и container в $imports , а затем добавить модификатор o-box--spacing-regular в $global-filter , например:

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container' // 'layout' ), component: ( // 'button', 'expander' // 'typography' ), utility: ( // 'alignments', // 'widths' ) ); $global-filter: ( 'o-box--spacing-regular' );

Это гарантирует, что стили для родительских элементов расширителя компилируются так, как если бы фильтрация не выполнялась.

Оригинальные и отфильтрованные таблицы стилей

Давайте сравним исходную таблицу стилей со всеми скомпилированными классами с отфильтрованной таблицей стилей, в которой был скомпилирован только CSS, требуемый компонентом расширения.

Стандарт
Таблица стилей Размер (КБ) Размер (gzip)
Оригинал 54,6кб 6,98 КБ
Отфильтровано 15,34 КБ (на 72% меньше) 4,91 КБ (на 29% меньше)
  • Оригинал: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
  • Отфильтровано: https://webdevluke.github.io/handlingunusedcss/dist/index.html

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

Стоит подчеркнуть, что сжатие gzip лучше работает с большими и повторяющимися файлами. Поскольку отфильтрованная таблица стилей является единственным доказательством концепции и содержит только CSS для компонента расширения, сжимать не так много, как в реальном проекте.

Если бы мы увеличили каждую таблицу стилей в 10 раз до размеров, более типичных для размера пакета CSS веб-сайта, разница в размерах файлов gzip была бы гораздо более впечатляющей.

10-кратный размер
Таблица стилей Размер (КБ) Размер (gzip)
Оригинал (10x) 892.07кб 75,70 КБ
Отфильтровано (10x) 209,45 КБ (на 77 % меньше) 19,47 КБ (на 74% меньше)

Отфильтрованная таблица стилей и UNCSS

Вот сравнение между отфильтрованной таблицей стилей и таблицей стилей, которая была запущена с помощью инструмента UNCSS.

Отфильтровано против UNCSS
Таблица стилей Размер (КБ) Размер (gzip)
Отфильтровано 15,34 КБ 4.91кб
UNCSS 12,89 КБ (на 16% меньше) 4,25 КБ (на 13 % меньше)

Инструмент UNCSS выигрывает здесь незначительно, поскольку он отфильтровывает CSS в каталогах generic и elements.

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

Подведение итогов

Итак, мы увидели, как — используя только Sass — вы можете получить больший контроль над тем, какие классы CSS компилируются при сборке. Это уменьшает количество неиспользуемого CSS в окончательной таблице стилей и ускоряет критический путь рендеринга.

В начале статьи я перечислил некоторые недостатки существующих решений, таких как UNCSS. Справедливо критиковать это решение, ориентированное на Sass, таким же образом, поэтому все факты на столе, прежде чем вы решите, какой подход лучше для вас:

Плюсы

  • Никаких дополнительных зависимостей не требуется, поэтому вам не нужно полагаться на чужой код.
  • Требуется меньше времени на сборку, чем альтернативы на основе Node.js, поскольку вам не нужно запускать безголовые браузеры для проверки кода. Это особенно полезно при непрерывной интеграции, поскольку у вас может быть меньше шансов увидеть очередь сборок.
  • Результаты в том же размере файла по сравнению с автоматизированными инструментами.
  • По умолчанию у вас есть полный контроль над тем, какой код фильтруется, независимо от того, как эти классы CSS используются в вашем коде. С альтернативами на основе Node.js вам часто приходится вести отдельный белый список, чтобы классы CSS, принадлежащие динамически внедряемому HTML, не отфильтровывались.

Минусы

  • Решение, ориентированное на Sass, безусловно, более практично, в том смысле, что вам нужно следить за переменными $imports и $global-filter . Помимо первоначальной настройки, рассмотренные нами альтернативы Node.js в значительной степени автоматизированы.
  • Если вы добавите классы CSS в $global-filter а затем удалите их из своего HTML, вам нужно не забыть обновить переменную, иначе вы будете компилировать CSS, который вам не нужен. С большими проектами, над которыми работают несколько разработчиков одновременно, это может быть непросто, если вы не планируете это должным образом.
  • Я бы не рекомендовал прикручивать эту систему к какой-либо существующей базе кода CSS, так как вам придется потратить довольно много времени на сбор зависимостей и применение миксина render() к МНОЖЕСТВУ классов. Эту систему гораздо проще внедрить с помощью новых сборок, где у вас нет существующего кода, с которым можно было бы бороться.

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