Збірка пройшла чисто. Тема виглядала правильно на моєму ноутбуці на всіх брейкпоінтах. Я задеплоїв, відкрив живий сайт на телефоні, і верстка зламалася: десктопна навігація з’явилася на мобільному, стилізований список навичок відрендерився звичайними буллетами, контент вилазив за правий край екрана, а кілька кольорів тексту не проходили за контрастністю. Нічого з цього не відбувалося локально. Код, який я задеплоїв, був байт у байт тим самим кодом, що я тестував.
Саме цей розрив краде години: браузер рендерить не той 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. Щоразу, коли щось рендериться правильно локально, але ламається на живому сайті, перевіряйте байти, перш ніж чіпати код.
- Підтвердьте вхідні дані. Відкрийте URL живого стилю з вкладки Network і прочитайте його. Не припускайте, що в браузера той файл, який ви задеплоїли.
- Порівняйте розміри. Велика різниця в обʼємі CSS між локаллю і продом — сильний сигнал, що шар оптимізації переписує ваш вивід.
- Знайдіть зламаний селектор. Якщо правила, яке ви дебажите, немає у відданому файлі, баг у конвеєрі доставки, а не у вихідному коді.
- Відтворіть з увімкненими оптимізаціями. Увімкніть кешування й оптимізацію CSS на стейджингу, щоб збій зʼявився до того, як його побачать користувачі.
- Виправляйте на правильному шарі. Виключіть уражені селектори з оптимізації або вимкніть функцію, замість переписувати робочий 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?
Не обовʼязково. Виграш у продуктивності реальний. Почніть із виключення конкретних селекторів чи шаблонів, що ламаються, і вимикайте функцію цілком лише тоді, коли виключень недостатньо. Залиште оптимізацію там, де вона безпечна.
Як протестувати це до деплою?
Запустіть стейджинг із тими ж налаштуваннями кешування й оптимізації, що й на проді, і тестуйте мобільні брейкпоінти й інтерактивні стани там. Баги, що залежать від оптимізації, не зʼявляться в неоптимізованому локальному середовищі.
Головне
- Коли CSS працює локально, але ламається на проді, перевіряйте байти, які отримує браузер, перш ніж дебажити код.
- LiteSpeed UCSS і схожі оптимізатори можуть вирізати правила, привʼязані до медіа-запитів, інтерактивних станів і динамічних класів.
- Обʼєм CSS, менший на проді, ніж локально, — сильний сигнал, що ваш стиль переписують.
- Постачайте захисні базові правила, щоб відсутній селектор чи інлайновий стиль не руйнували верстку.
- Дзеркальте продакшен-кешування на стейджингу й очищайте кеш і перевіряйте відданий CSS після кожного деплою.
Повʼязані статті
- AI-агенти в робочому процесі розробки описує безпечний цикл деплою й перевірки, до якого належить такий продакшен-баг.
- Субагенти Claude Code й оптимізація токенів показує browser-tester агента, якого я використовую, щоб ловити фронтенд-регресії до продакшену.
- Як витік секрету в Git перетворився на чотирирічне переписування історії — ще одна історія «виглядало добре до продакшену», з дуже іншим радіусом ураження.




