Oleksii Siniaiev
RUUKESEN
Post page navigation

Blog article Articles 9 min read

CSS works locally but breaks in production: how LiteSpeed UCSS strips your styles

A real debugging story: the theme worked locally but broke in production because LiteSpeed UCSS stripped CSS rules. How to compare what the browser receives, the four bugs it caused, and how to prevent it.

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

The build was clean. The theme looked right on my laptop at every breakpoint. I deployed, opened the live site on my phone, and the layout was broken: the desktop navigation showed up on mobile, a styled skills list rendered as plain bullets, content spilled off the right edge of the screen, and a few text colors failed contrast. None of that happened locally. The code I deployed was byte-for-byte the code I had tested.

This is the gap that wastes hours: the browser is not rendering the CSS you wrote. It is rendering the CSS your host decided to serve. On a LiteSpeed stack with unused-CSS optimization turned on, those two things are not the same file. This article walks through how I found that, the four bugs it caused, and the repeatable method for debugging production CSS when “it works on my machine” stops being good enough.

Quick answer: why CSS works locally but breaks in production

If your CSS works locally but breaks in production, the usual cause is not your code. It is a caching or optimization layer between your server and the browser that rewrites, strips, or reorders your stylesheet. LiteSpeed Cache with the UCSS (unused CSS) and CCSS (critical CSS) features is a common culprit: it scans a page, decides which rules are “unused”, and serves a trimmed stylesheet. Rules attached to classes added later, to dynamic states, or to pages the scanner never visited get removed. The fix is to compare what the browser actually downloads against what you deployed, then exclude the broken selectors from optimization or disable the feature.

The setup: local and production were not the same environment

My local environment runs WordPress through Docker with no caching layer. Production runs on Hostinger with LiteSpeed and LiteSpeed Cache enabled, including page caching, CSS minification, CSS combination, and unused-CSS generation. Locally, the browser requests style.css and gets exactly the file on disk. In production, the browser requests an optimized, machine-generated stylesheet that LiteSpeed built by guessing which rules the page needs.

That difference is invisible until you go looking for it. The HTML was identical. The deploy was a clean git pull. Yet the rendered result was wrong. The first instinct is to blame your own media queries or a specificity bug. The faster move is to confirm whether the browser even received the rule you are debugging.

The one diagnostic that saved the day: compare bytes, not behavior

The breakthrough was boring and quantitative. I opened DevTools on both environments, went to the Network tab, and looked at the actual CSS payload.

  • Local: the stylesheet was 53,378 bytes.
  • Production: the stylesheet the browser downloaded was 38,886 bytes.

Almost 15 KB of CSS was missing in production. That single comparison reframed the entire problem. I was not chasing a layout bug. I was chasing a stylesheet that had been edited by the host before it reached the browser. Counting parsed rules confirmed it: the browser had loaded roughly 250 of the 311 rules in my source file. The other 61 were gone.

You can run that check yourself in the DevTools console:

// Count CSS rules the browser actually parsed
[...document.styleSheets]
  .map(s => { try { return s.cssRules.length } catch (e) { return 0 } })
  .reduce((a, b) => a + b, 0);

Compare the number locally and in production. If production is lower, something is stripping your CSS. View the served stylesheet directly (open the CSS URL from the Network tab) and search for a selector you know is broken. If it is not in the file, the problem is upstream of your code.

The four bugs, and what was actually wrong

Bug 1: the desktop navigation showed on mobile

The mobile menu toggle was gone and the full desktop nav was visible on a narrow screen. My first guess was a broken media query. It was not. The media-query block that hid the desktop nav below a breakpoint had been removed from the served stylesheet entirely. UCSS decided those rules were unused because the scanner evaluated the page at a desktop viewport, where the desktop nav is visible and the mobile toggle is hidden. From that single snapshot, the mobile rules looked dead. They were not. They only applied at a viewport the scanner never tested.

Bug 2: a styled list rendered as plain bullets

A skills list that should render as styled chips came out as a default bulleted list. Same root cause, different trigger. The classes that styled the list were present in the HTML, but the rules targeting them were among the 61 that got stripped. Unused-CSS detection is fragile with content that is conditional, paginated, or rendered only in certain states. If the scanner does not see the class in the exact context it expects, it treats the rule as removable.

Bug 3: content overflowed the viewport on mobile

On mobile, blocks pushed past the right edge and produced a horizontal scrollbar. This one was not LiteSpeed. It was the editor: Gutenberg had written an inline width onto a block, and an inline style wins against a stylesheet rule. With the stylesheet already trimmed, there was nothing left to constrain the element. The fix was defensive CSS that does not depend on optimization surviving:

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

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

Defensive base rules like max-width: 100% and a container overflow-x: hidden are cheap insurance. They protect the layout even when a more specific rule is missing or an inline style sneaks in.

Bug 4: text colors failed WCAG contrast

A few muted text colors and link states did not meet WCAG AA contrast against their backgrounds. This was a real code issue, not a caching one, but production is where it became visible because the surrounding styles had shifted. I adjusted the color tokens until body text and interactive states cleared the 4.5:1 ratio for normal text. Contrast is one of the easiest accessibility wins to verify: the DevTools color picker and Lighthouse both flag failures directly.

The root cause: how “unused” CSS optimization misfires

LiteSpeed UCSS and CCSS exist for a good reason. Shipping less CSS improves Largest Contentful Paint and reduces render-blocking resources. The feature loads a page, watches which rules apply, and generates a slimmer stylesheet. The problem is the word “applies”. A rule applies only in the context the generator observed:

  • Viewport-specific rules behind media queries look unused if the page is scanned at one screen size.
  • State-specific rules for hover, focus, open menus, or expanded accordions look unused if the state never fires during the scan.
  • Dynamic classes added by JavaScript after load are invisible to a static or single-pass scan.
  • Template-specific rules for pages the generator never visited are simply never accounted for.

The result is a stylesheet that is correct for one frozen snapshot of one page and wrong for every variation the snapshot did not capture. Mobile layouts, interactive states, and conditional content are exactly the cases that break.

A repeatable method for debugging production CSS

The lesson generalizes well beyond LiteSpeed. Whenever something renders correctly locally but breaks live, verify the bytes before you touch the code.

  1. Confirm the input. Open the live stylesheet URL from the Network tab and read it. Do not assume the browser has the file you deployed.
  2. Compare sizes. A large difference in CSS payload between local and production is a strong signal that an optimization layer is rewriting your output.
  3. Search for the broken selector. If the rule you are debugging is not in the served file, the bug is in the delivery pipeline, not your source.
  4. Reproduce with optimizations on. Turn caching and CSS optimization on in a staging environment so the failure shows up before users see it.
  5. Fix at the right layer. Exclude the affected selectors from optimization, or disable the feature, instead of rewriting working CSS to dodge a stripped rule.

How to prevent it on a LiteSpeed or CDN stack

Once you know the failure mode, prevention is straightforward.

RiskWhat to do
UCSS strips media-query and state rulesAdd critical selectors to the UCSS allowlist, or disable UCSS for templates with heavy responsive or interactive CSS.
Dynamic classes get removedExclude JavaScript-toggled classes from optimization, or render them server-side so the scanner sees them.
Inline styles override trimmed CSSShip defensive base rules (max-width: 100%, container overflow-x: hidden) that hold up without the full stylesheet.
Bugs only appear in productionMirror production caching in staging, and test mobile and interactive states with optimization enabled.
Stale optimized CSS after a deployPurge the CSS and page cache as part of the release, then re-check the served payload.

After every deploy that touches CSS, I now purge the LiteSpeed cache and immediately re-check the served stylesheet size against local. It takes thirty seconds and it catches exactly this class of bug before anyone else sees it.

FAQ

Why does my CSS work locally but not in production?

Most often because production sits behind a caching or optimization layer that rewrites your stylesheet. Minification, CSS combination, and unused-CSS removal can drop rules your pages actually need. Compare the CSS the browser downloads in each environment before assuming the bug is in your code.

What is LiteSpeed UCSS and why does it remove CSS?

UCSS (unused CSS) is a LiteSpeed Cache feature that generates a trimmed stylesheet containing only the rules it believes a page uses. It improves load performance, but it can misclassify rules that apply only at certain viewports, in certain states, or to dynamically added classes, and remove them.

How do I know if my host is stripping CSS?

Open DevTools, go to the Network tab, and compare the size of the served stylesheet in production against your local file. Then open the production CSS URL and search for a selector you know is broken. If it is missing, the delivery layer removed it.

Should I just disable CSS optimization?

Not necessarily. The performance benefit is real. Start by excluding the specific selectors or templates that break, and only disable the feature wholesale if exclusions are not enough. Keep the optimization where it is safe.

How do I test for this before deploying?

Run a staging environment with the same caching and optimization settings as production, and test mobile breakpoints and interactive states there. Bugs that depend on optimization will not show up in an unoptimized local setup.

Key takeaways

  1. When CSS works locally but breaks in production, verify the bytes the browser receives before you debug your code.
  2. LiteSpeed UCSS and similar optimizers can strip rules tied to media queries, interactive states, and dynamic classes.
  3. A CSS payload that is smaller in production than locally is a strong signal that your stylesheet is being rewritten.
  4. Ship defensive base rules so a missing selector or an inline style does not destroy the layout.
  5. Mirror production caching in staging, and purge the cache and re-check the served CSS after every deploy.

Share this article

LinkedIn X Email

Explore more

May 30, 2026

Claude Code setup: a beginner’s guide to the AI coding CLI

Step-by-step guide to installing Claude Code, configuring CLAUDE.md, understanding permissions, and running your first…

May 20, 2026

Claude Code Subagents: Copy-Paste Agents for Safer, Cheaper Workflows

A practical 2026 guide to Claude Code subagents with copy-paste .claude/agents examples for explorer,…

May 17, 2026

AI Agents in the Development Workflow: A Practical Safety Guide for Developers

A practical May 2026 guide to using AI agents in software development safely: Claude…