Файл стилей большого проекта почти всегда тяжелее, чем должен быть. В нём лежат два фреймворка, следы трёх переделанных дизайнов, стили блоков, которых давно нет, и утилиты, добавленные "на всякий случай". Типичный итог для сайта с историей в пять лет выглядит так: около 300 КБ css, из которых браузер реально применяет от силы десятую часть. Остальное едет к пользователю по сети, разбирается движком и задерживает первую отрисовку страницы. PurgeCSS убирает эту избыточность на уровне сборки: инструмент сканирует файлы проекта, собирает имена классов, которые действительно встречаются в разметке и коде, и удаляет из таблиц стилей всё, что нигде не нашлось. Ниже разобрана рабочая процедура очистки крупного проекта, настройка под webpack и обычные сборочные скрипты, а также ловушки с динамическими классами, из-за которых команды чаще всего откатывают результат обратно.

Откуда в больших проектах берётся мёртвый CSS

Первый источник лишнего веса – готовые фреймворки. Полная минифицированная сборка Bootstrap весит около 230 КБ, при этом типичный сайт использует каркас сетки, кнопки и формы, а карусели, аккордеоны, тултипы и прогресс-бары так и остаются в файле мёртвым грузом. Подключать фреймворк по частям долго и неудобно, поэтому в css попадают все сотни компонентов сразу, включая те, что не понадобятся никогда.

Второй источник – годы разработки и переделки. Редизайн прошёл, старые блоки убрали из вёрстки, а их стили остались "на всякий случай". Новый разработчик не знает, живы селекторы или нет, боится что-то сломать и спокойно дописывает свои классы рядом. Через несколько итераций файл разрастается до 300-500 КБ, и половина его содержимого относится к интерфейсам, которых в проекте давно не существует.

Третий источник – сами разработчики и их привычки. Прототип собирается на коленке с десятком черновых утилитарных классов, а потом уезжает в продакшн вместе со всеми экспериментами. Плюс копипаст: кусок css переезжает из старого проекта со своими специфичными селекторами и приживается в новом.

Масштаб бедствия легко увидеть в DevTools. На вкладке Coverage достаточно запустить запись и обновить страницу, и инструмент покажет, сколько байт css реально применялось. На старых крупных сайтах цифра стабильно держится в районе 70-90% неиспользуемых стилей. Это не экзотика и не преувеличение, а норма для проекта, где файл стилей никто не чистил годами.

Установка PurgeCSS и первый пробный запуск

Инструмент ставится одной командой:

npm install purgecss --save-dev

Для быстрой проверки без конфигурационного файла есть CLI. Он принимает пути к css и к файлам контента, а очищенный результат пишет в отдельную папку:

npx purgecss --css dist/css/main.css --content "src/**/*.html" "src/**/*.js" --output dist/css/clean/

Логика работы простая. Инструмент разбирает содержимое перечисленных файлов, вытаскивает из них все токены, похожие на имена классов, идентификаторы и теги, а затем удаляет из css селекторы, которых в контенте не нашлось. Связи между селекторами при этом учитываются: если селектор .btn остался, то привязанный к нему .btn:hover тоже сохраняется, потому что очистка работает по базовому имени.

Первый запуск в обязательном порядке делается на копии проекта или в отдельной ветке git, а не на рабочей сборке. Оригинальный файл стилей остаётся нетронутым до тех пор, пока очищенный вариант не проверен на всех ключевых страницах. Ещё один практический момент: свежие версии инструмента требуют современного Node, поэтому перед установкой стоит убедиться, что окружение проекта не осталось где-нибудь на Node 12.

Конфигурация для проекта с шаблонами PHP и JavaScript

Для крупного проекта настройки удобнее держать в файле purgecss.config.js в корне репозитория. В массиве content перечисляются все места, где могут встретиться имена классов: вёрстка, скрипты, серверные шаблоны PHP, компоненты фреймворков. В массиве css указываются файлы, которые нужно почистить, а параметр output задаёт папку для результата:

module.exports = {
  content: [
    "./src/**/*.html",
    "./src/**/*.js",
    "./templates/**/*.php",
    "./components/**/*.vue"
  ],
  css: ["./dist/css/main.css"],
  output: "./dist/css/clean/",
  safelist: {
    standard: ["active", "show", "open"]
  }
};

Шаблоны путей со звёздочками принципиальны. Запись ./src/**/*.js захватывает файлы на любой глубине вложенности, и это важно, потому что имена классов встречаются не только в вёрстке, но и во вложенных модулях, и в конфигурациях виджетов. Шаблоны PHP вынесены отдельной строкой тоже не случайно: на сайтах с самописными CMS часть классов живёт именно в серверных шаблонах, и если эти файлы не указать, очистка срежет стили вполне себе живых блоков.

Запуск сводится к одной строке, конфигурационный файл подхватывается автоматически:

npx purgecss --config purgecss.config.js

Интеграция очистки в webpack и в сборочный скрипт

Гонять очистку руками перед каждым релизом быстро надоедает, поэтому её встраивают в сборку. В проектах на webpack для этого существует отдельный плагин purgecss-webpack-plugin:

const glob = require("glob");
const PurgecssPlugin = require("purgecss-webpack-plugin");

module.exports = {
  plugins: [
    new PurgecssPlugin({
      paths: glob.sync("./src/**/*", { nodir: true }),
      safelist: { standard: [/^is-/, /^swiper-/] }
    })
  ]
};

Плагин добавляется в продакшн-конфигурацию и срабатывает на этапе сборки, когда стили уже извлечены в отдельные файлы. Локальная разработка остаётся нетронутой: в dev-режиме весь css на месте, и горячая перезагрузка работает как обычно, без сюрпризов посреди вёрстки.

Если проект собирается без webpack, очистку встраивают в npm-скрипт:

"scripts": {
  "build:css": "purgecss --config purgecss.config.js",
  "build": "npm run build:assets && npm run build:css"
}

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

Динамические классы, главная ловушка PurgeCSS

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

Первый случай – классы, собранные конкатенацией в JavaScript. В коде написано el.className = "btn btn-" + size, где переменная size в рантайме принимает значения lg или sm. PurgeCSS ищет в файлах готовые токены, литерала btn-lg в коде нет, и селектор .btn-lg удаляется вместе со всеми свойствами. Кнопка большого размера после очистки внезапно теряет оформление, хотя в момент прогона сборка выглядела абсолютно рабочей.

Второй случай – классы из базы данных и CMS. Администратор вписывает в поле блока класс promo-highlight, значение сохраняется в базе и подставляется в разметку на сервере. В репозитории этой строки нет, она существует только в данных. Инструмент сканирует файлы, базу не видит, и стиль для promo-highlight исчезает из продакшн-версии css.

Третий случай – классы, которые библиотеки добавляют в рантайме. Слайдер вешает на активный кадр класс swiper-slide-active, валидатор помечает поле классом invalid, модальное окно получает is-open. В момент сканирования этих классов в разметке ещё нет, они появляются только в процессе работы страницы, когда сборка давно закончилась.

Отдельная категория – модификаторы состояний: active, selected, show, отмеченные чекбоксы, раскрытые меню. Всё, что добавляется скриптами по событию, для статического анализа выглядит неиспользуемым. Туда же относятся селекторы по атрибутам вида [class^="icon-"]: поведение очистки на таких конструкциях нестабильно, и их разумнее сразу выводить из зоны обработки.

Финал у всех трёх случаев одинаковый: элемент жив, js отрабатывает, класс добавляется, но описания этого класса в css больше нет. Интерфейс разваливается в самый неподходящий момент, как правило уже после выкладки на продакшн, когда до разработчика сигнал дойдёт через жалобы пользователей.

Safelist и отдельный файл для защиты нужных стилей

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

safelist: {
  standard: ["active", "show", "invalid", "is-open"],
  greedy: [/^swiper-slide/, /^icon-/]
}

Записи из standard защищают классы целиком, greedy расширяет защиту на все селекторы, где встречается шаблон, включая вложенные состояния. Удобная стратегия для крупного проекта – защищать не отдельные имена, а префиксы: /^is-/, /^has-/, /^js-/ покрывают практически всю динамическую семантику, если команда договорилась именовать состояния единообразно и придерживается договорённости.

Второй путь работает даже там, где safelist не спасает. Всё, что относится к состояниям, слайдерам, модальным окнам и классам из CMS, переносится в отдельный файл styles-dynamic.css. Он подключается собственным тегом link и просто не подаётся на вход очистке: в конфигурации значится только основной main.css. Цена решения – один лишний запрос к серверу и несколько килобайт, обычно 3-5 КБ, что на фоне общего выигрыша выглядит символически.

Третий приём пригодится сайтам с CMS. На этапе деплоя скрипт выгружает все значения полей, где могут лежать классы, в файл cms-classes.json, а этот файл добавляется в массив content. Очистка начинает видеть классы из базы как обычные токены, и safelist остаётся коротким, вместо того чтобы разрастаться в бесконечный список исключений.

Цифры экономии веса и порядок проверки результата

Ради чего всё затевалось? Типичная картина крупного проекта выглядит так: main.css весил 300 КБ, после прогона PurgeCSS осталось 25-40 КБ. Сокращение в 7-12 раз, то есть порядка 85-90% исходного веса, при том что страница внешне не изменилась ни на пиксель. Даже после gzip, сжимающего css примерно в 4-5 раз, разница по сети остаётся драматичной: около 65 КБ против 7-8 КБ.

Эффект сильнее всего заметен на мобильном трафике. На соединении со скоростью около 125 КБ/с, характерном для слабого 3G, файл в 300 КБ будет загружаться примерно 2,5 секунды, а файл в 30 КБ укладывается в четверть секунды. Поскольку css по умолчанию блокирует первую отрисовку, эта разница напрямую попадает в метрики скорости сайта и в позиции поиска, где скорость загрузки давно учитывается. Добавляется выигрыш в парсинге: движку проще разобрать несколько тысяч строк селекторов, чем десятки тысяч, и меньше работы при сопоставлении правил с элементами страницы.

Цифры стоит перепроверить на конкретном проекте через ту же вкладку Coverage: после очистки доля неиспользуемых стилей должна упасть с 70-90% до единиц процентов. Если осталось 30-40%, значит, в css живёт много контента, которого нет в перечисленных путях content, и это повод пересмотреть конфигурацию, а не смириться.

Сама проверка результата сводится к следующему порядку действий:

  1. Открыть в браузере 10-15 ключевых страниц сайта и пролистать каждую целиком, от шапки до подвала;
  2. Проверить интерактивные элементы: модальные окна, выпадающие меню, табы, слайдеры и всплывающие подсказки;
  3. Пройтись по формам и спровоцировать ошибки валидации, чтобы увидеть состояния полей и кнопок;
  4. Сравнить скриншоты страниц до очистки и после неё, обращая внимание на отступы и типографику;
  5. Просмотреть итоговый css и поискать в нём селекторы, которые точно должны были сохраниться.

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

И у регулярной очистки есть побочный эффект, который со временем оказывается ценнее самой экономии. Когда при каждом деплое из css удаляется всё лишнее, проект перестаёт копить мёртвые стили годами, а файл становится обозримым: в нём видно только то, что действительно работает на страницах. Разобраться в таком css, найти нужное правило и не задеть соседнее можно за минуты, а не за вечер с поиском по истории правок.