Алексей Синяев
RUUKESEN
Навигация по странице статьи

Статья блога Статьи 1 мин чтения

CSS работает локально, но ломается на проде: как LiteSpeed UCSS вырезает ваши стили

Реальная история дебага: тема работала локально, но сломалась на проде, потому что LiteSpeed UCSS вырезал правила CSS. Как сравнить то, что получает браузер, четыре бага и как это предотвратить.

Дебаг продакшен-CSS: порівняння байтів таблиці стилів, яку завантажує браузер, із задеплоєним вихідником
Содержание

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

Именно этот разрыв крадёт часы: браузер рендерит не тот CSS, который вы написали. Он рендерит тот CSS, который решил отдать ваш хостинг. На стеке LiteSpeed с включённой оптимизацией неиспользуемого CSS это не один и тот же файл. В этой статье я покажу, как я это нашёл, какие четыре бага это вызвало, и какой повторяемый метод дебага продакшен-CSS работает, когда «у меня на машине всё работает» уже недостаточно.

Короткий ответ: почему CSS работает локально, но ломается на проде

Если ваш CSS работает локально, но ломается на проде, причина обычно не в вашем коде. Это слой кеширования или оптимизации между сервером и браузером, который переписывает, вырезает или переставляет ваш стиль. LiteSpeed Cache с функциями UCSS (неиспользуемый CSS) и CCSS (критический CSS) — частый виновник: он сканирует страницу, решает, какие правила «не используются», и отдаёт обрезанный стиль. Правила, привязанные к классам, добавленным позже, к динамическим состояниям или к страницам, которые сканер не посещал, удаляются. Решение: сравнить то, что браузер реально загружает, с тем, что вы задеплоили, а затем исключить сломанные селекторы из оптимизации или отключить эту функцию.

Исходные данные: локальное окружение и прод не были одинаковыми

Моё локальное окружение работает на WordPress через Docker без слоя кеширования. Прод работает на Hostinger с LiteSpeed и включённым LiteSpeed Cache, включая кеширование страниц, минификацию CSS, объединение CSS и генерацию неиспользуемого CSS. Локально браузер запрашивает style.css и получает ровно тот файл, что лежит на диске. На проде браузер запрашивает оптимизированный, сгенерированный машиной стиль, который LiteSpeed собрал, угадав, какие правила нужны странице.

Эта разница невидима, пока вы не начнёте её искать. HTML был идентичен. Деплой был чистым git pull. А результат рендеринга был неправильный. Первый инстинкт — обвинить собственные медиа-запросы или баг со специфичностью. Более быстрый ход — подтвердить, получил ли браузер вообще то правило, которое вы дебажите.

Одна диагностика, что спасла день: сравнивайте байты, а не поведение

Прорыв был скучным и количественным. Я открыл DevTools в обоих окружениях, перешёл на вкладку Network и посмотрел на реальный объём CSS.

  • Локально: стиль весил 53 378 байт.
  • На проде: стиль, который загружал браузер, весил 38 886 байт.

Почти 15 КБ CSS отсутствовали на проде. Это единственное сравнение переформулировало всю проблему. Я гнался не за багом вёрстки. Я гнался за стилем, который хостинг отредактировал до того, как он дошёл до браузера. Подсчёт распарсенных правил это подтвердил: браузер загрузил примерно 250 из 311 правил моего исходного файла. Остальные 61 исчезли.

Эту проверку можно сделать самому в консоли DevTools:

// Посчитать CSS-правила, которые браузер реально распарсил
[...document.styleSheets]
  .map(s => { try { return s.cssRules.length } catch (e) { return 0 } })
  .reduce((a, b) => a + b, 0);

Сравните число локально и на проде. Если на проде меньше, что-то вырезает ваш CSS. Откройте отданную таблицу стилей напрямую (откройте URL CSS из вкладки Network) и найдите селектор, который точно сломан. Если его нет в файле, проблема выше вашего кода.

Четыре бага и что на самом деле было не так

Баг 1: десктопная навигация показывалась на мобильном

Кнопка мобильного меню исчезла, а полная десктопная навигация была видна на узком экране. Первая догадка — сломанный медиа-запрос. Это было не так. Блок медиа-запроса, прятавший десктопную навигацию ниже брейкпоинта, был полностью удалён из отданного стиля. UCSS решил, что эти правила не используются, потому что сканер оценивал страницу на десктопном вьюпорте, где десктопная навигация видима, а мобильная кнопка скрыта. С этого единственного снимка мобильные правила выглядели мёртвыми. Они не были мёртвыми. Они лишь применялись на вьюпорте, который сканер никогда не тестировал.

Баг 2: стилизованный список рендерился как обычные буллеты

Список навыков, который должен был рендериться как стилизованные чипы, вышел как обычный маркированный список. Та же первопричина, другой триггер. Классы, стилизовавшие список, были в HTML, но правила, таргетившие их, были среди тех 61, что вырезались. Обнаружение неиспользуемого CSS хрупко с контентом, который условный, постраничный или рендерится только в определённых состояниях. Если сканер не видит класс в точном контексте, который ожидает, он считает правило удаляемым.

Баг 3: контент вылезал за вьюпорт на мобильном

На мобильном блоки выходили за правый край и давали горизонтальный скролл. Это был не LiteSpeed. Это был редактор: Gutenberg прописал инлайновый width блоку, а инлайновый стиль побеждает правило из стиля. Поскольку стиль уже был обрезан, не осталось ничего, что бы ограничивало элемент. Решение — защитный CSS, не зависящий от того, выживет ли оптимизация:

.entry-content img,
.entry-content figure,
.entry-content .wp-block-image {
  max-width: 100%;
  height: auto;
}

.entry-content {
  overflow-x: hidden;
}

Защитные базовые правила вроде max-width: 100% и контейнерного overflow-x: hidden — это дешёвая страховка. Они защищают вёрстку даже тогда, когда более специфичное правило отсутствует или пролез инлайновый стиль.

Баг 4: цвета текста не проходили по контрастности WCAG

Несколько приглушённых цветов текста и состояний ссылок не соответствовали контрастности WCAG AA к фону. Это был реальный баг в коде, а не в кешировании, но именно на проде он стал видимым, потому что соседние стили сдвинулись. Я подправил цветовые токены, пока основной текст и интерактивные состояния не прошли соотношение 4.5:1 для обычного текста. Контраст — одна из самых лёгких побед в доступности для проверки: и пипетка цвета в DevTools, и Lighthouse помечают провалы напрямую.

Первопричина: как оптимизация «неиспользуемого» CSS промахивается

LiteSpeed UCSS и CCSS существуют не просто так. Меньше CSS улучшает Largest Contentful Paint и уменьшает блокирующие рендеринг ресурсы. Функция загружает страницу, смотрит, какие правила применяются, и генерирует более тонкий стиль. Проблема в слове «применяются». Правило применяется только в том контексте, который наблюдал генератор:

  • Правила под конкретный вьюпорт за медиа-запросами выглядят неиспользуемыми, если страницу сканируют на одном размере экрана.
  • Правила под конкретное состояние для hover, focus, открытых меню или развёрнутых аккордеонов выглядят неиспользуемыми, если состояние не сработало во время скана.
  • Динамические классы, добавленные JavaScript после загрузки, невидимы для статического скана в один проход.
  • Правила под конкретный шаблон для страниц, которые генератор не посещал, просто никогда не учитываются.

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

Повторяемый метод дебага продакшен-CSS

Урок хорошо обобщается далеко за пределы LiteSpeed. Всякий раз, когда что-то рендерится правильно локально, но ломается на живом сайте, проверяйте байты, прежде чем трогать код.

  1. Подтвердите входные данные. Откройте URL живого стиля из вкладки Network и прочитайте его. Не предполагайте, что у браузера тот файл, что вы задеплоили.
  2. Сравните размеры. Большая разница в объёме CSS между локалью и продом — сильный сигнал, что слой оптимизации переписывает ваш вывод.
  3. Найдите сломанный селектор. Если правила, которое вы дебажите, нет в отданном файле, баг в конвейере доставки, а не в исходном коде.
  4. Воспроизведите с включёнными оптимизациями. Включите кеширование и оптимизацию CSS на стейджинге, чтобы сбой появился до того, как его увидят пользователи.
  5. Чините на правильном слое. Исключите затронутые селекторы из оптимизации или отключите функцию, вместо того чтобы переписывать рабочий CSS ради обхода вырезанного правила.

Как это предотвратить на стеке LiteSpeed или CDN

Когда вы знаете режим сбоя, предотвращение простое.

РискЧто делать
UCSS вырезает правила медиа-запросов и состоянийДобавьте критические селекторы в вайтлист UCSS или отключите UCSS для шаблонов с тяжёлым адаптивным или интерактивным CSS.
Динамические классы удаляютсяИсключите классы, переключаемые через JavaScript, из оптимизации или рендерите их на сервере, чтобы сканер их видел.
Инлайновые стили перекрывают обрезанный CSSПоставляйте защитные базовые правила (max-width: 100%, контейнерный overflow-x: hidden), держащиеся без полного стиля.
Баги появляются только на продеЗеркальте продакшен-кеширование на стейджинге и тестируйте мобильные и интерактивные состояния с включённой оптимизацией.
Устаревший оптимизированный CSS после деплояОчищайте кеш CSS и страниц как часть релиза, затем проверяйте отданный объём заново.

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

Частые вопросы

Почему мой CSS работает локально, но не на проде?

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

Что такое LiteSpeed UCSS и почему он удаляет CSS?

UCSS (неиспользуемый CSS) — функция LiteSpeed Cache, генерирующая обрезанный стиль только из правил, которые, по её мнению, страница использует. Она улучшает производительность загрузки, но может ошибочно классифицировать правила, применяющиеся только на определённых вьюпортах, в определённых состояниях или к динамически добавленным классам, и удалить их.

Как понять, вырезает ли хостинг CSS?

Откройте DevTools, перейдите на вкладку Network и сравните размер отданного стиля на проде с локальным файлом. Затем откройте URL продакшен-CSS и найдите селектор, который точно сломан. Если его нет, слой доставки его убрал.

Может, просто отключить оптимизацию CSS?

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

Как протестировать это до деплоя?

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

Главное

  1. Когда CSS работает локально, но ломается на проде, проверяйте байты, которые получает браузер, прежде чем дебажить код.
  2. LiteSpeed UCSS и похожие оптимизаторы могут вырезать правила, привязанные к медиа-запросам, интерактивным состояниям и динамическим классам.
  3. Объём CSS, меньший на проде, чем локально, — сильный сигнал, что ваш стиль переписывают.
  4. Поставляйте защитные базовые правила, чтобы отсутствующий селектор или инлайновый стиль не рушили вёрстку.
  5. Зеркальте продакшен-кеширование на стейджинге и очищайте кеш и проверяйте отданный CSS после каждого деплоя.

Связанные статьи

Поделиться статьей

LinkedIn X Email

Смотрите также

May 30, 2026

Настройка Claude Code: руководство для начинающих

Пошаговое руководство по установке Claude Code, настройке CLAUDE.md, модели разрешений и первым реальным задачам.…

May 20, 2026

Claude Code субагенты: готовые агенты для более безопасного и дешевого workflow

Практический гайд 2026 по субагентам Claude Code: готовые .claude/agents примеры для explorer, planner, reviewer,…

May 17, 2026

AI-агенты в рабочем процессе разработчика: практический гайд

Практический гайд на май 2026 по AI-агентам в разработке: Claude Code, Codex, Cursor, Gemini,…