# How to remove unused CSS

> Most themes ship a stylesheet where any given page uses a fraction of the rules. Prune the rest per URL — with a safelist and real-visitor learning to keep it honest.

- Category: Optimization
- Canonical: https://xspeedcache.com/docs/unused-css/

---

A page-builder theme can ship hundreds of kilobytes of CSS covering every block, layout and widget it supports — while any given page uses a small fraction of it. **Unused CSS** works out which rules a URL actually needs and serves only those. It's typically the single biggest transfer reduction available on this panel, and also the one that most deserves checking before you trust it.

> **Where to find it**
>
> 1. In your WordPress admin, click **xSpeed Cache** in the left menu.
> 2. In the xSpeed Cache sidebar, open the **Optimization** group.
> 3. Click the **CSS & JavaScript** card, then the **Unused CSS** tab.
>
> *Shortcut: open `wp-admin/admin.php?page=xspeed#/performance/minify/unused` directly.*
>
> ⭐ This tab is part of **xSpeed Pro**.

![Unused CSS status and generation](https://xspeedcache.com/_astro/unused-css-generate.DYdKjNW-.png)

1. **Status** — whether it's on, how many URLs are cached, and total size.
2. **Build one now (optional)** — capture in the browser, or generate on the server, instead of waiting for an automatic build.

---

## Settings at a glance

| Setting | Default | What it does |
| --- | --- | --- |
| **Enable Unused CSS** | Off | Replace stylesheets with pruned inline CSS. |
| **Learn from real visitors** | Off | Refine the analysis using real page views. |
| **Rendering service URL** | Empty | A self-hosted renderer to measure pages with. Empty = rendering is brokered through the xSpeed Hub. |
| **Rendering service token** | Empty | Bearer token for that service, when it requires one. |
| **Never prune these selectors** | Empty | Safelist of selector fragments to always keep. |

---

## How it works

Analysis runs **per URL**, like [Critical CSS](https://xspeedcache.com/docs/critical-css/), because different templates use different rules.

**Builds happen on their own.** A visitor landing on a page with no pruned CSS queues one, and WordPress cron builds it a few minutes later — you don't have to press anything. The button on the panel is for building a page right now instead of waiting. Entries rebuild themselves when your theme, a plugin, or a plugin version changes, so they don't go stale as the site does — and **Purge all** leaves them alone, because they aren't a render cache.

**A doubtful entry is refused, not trusted.** An entry that's stale, empty or implausibly small is rejected rather than used, so a bad build can't strip every stylesheet from a page and leave it unstyled. And only one CSS strategy owns a page: where Unused CSS has a usable entry, [Critical CSS](https://xspeedcache.com/docs/critical-css/) stands down.

When enabled, **each run of adjacent stylesheets is replaced, in place, by an inline `<style>` block holding just that run's pruned CSS**. That "in place" detail matters more than it sounds: nothing moves relative to the inline blocks around it, so the cascade is preserved. CSS is order-sensitive, and a tool that reorders your styles will break things in ways that are miserable to debug. This one doesn't.

**Some stylesheets are never touched**, and knowing the list saves you time:

- Sheets marked `data-xspeed-keep`
- Sheets marked `data-no-optimize`
- Disabled sheets
- Sheets carrying an `integrity` attribute
- Cross-origin stylesheets

The integrity and cross-origin exclusions are structural: pruning a sheet with a subresource-integrity hash would invalidate it, and a cross-origin sheet isn't xSpeed's to rewrite.

---

## Two ways to generate

Same choice as [Critical CSS](https://xspeedcache.com/docs/critical-css/):

| | Capture in browser | Generate on server |
| --- | --- | --- |
| **How** | Opens the page in a hidden frame in this tab and records which rules it actually uses | Has a real browser render the page — brokered by the Hub, or through your own [Rendering service URL](#rendering-service-url) — and prunes from what it measured |
| **Accuracy** | Sees what JavaScript builds and what really renders, at your screen size | Also a real render |
| **Needs** | The tab kept open while it runs | Nothing — works headlessly |
| **Fails when** | A security plugin blocks framing | The render fails — reported, never swapped for a guess |

**Capture in browser** runs in *your* browser, so it needs the tab left open. **Generate on server** is the choice when your browser can't frame the page. External tooling can also push pruned CSS in via the upload endpoint if you generate it in a build pipeline.

> ⚠️ **A failed render is reported, not replaced with a guess.** If a render fails on a licensed install, xSpeed won't quietly fall back to reading the served markup. The failure shows on the panel, from WP-CLI and in [the activity log](https://xspeedcache.com/docs/logs/), and automatic builds pause for a cooldown before trying again.

---

## Settings

![The Unused CSS settings](https://xspeedcache.com/_astro/unused-css-settings.DbHbCO-o.png)

1. **Enable Unused CSS** — the master switch.
2. **Learn from real visitors** — refine using real page views.
3. **Rendering service URL** — measure pages in a real browser instead of reading markup.
4. **Rendering service token** — bearer token, if your service needs one.
5. **Never prune these selectors** — the safelist.

### Learn from real visitors

Off by default. It loads a small front-end script reporting which selectors each page actually uses, from real browsers. **Reports carry selector names only — never CSS, and never visitor data.**

The safety mechanism is worth understanding: **several visitors have to agree before anything is rebuilt**. One visitor's session doesn't rewrite your CSS. That guards against a single unusual page view — someone with an extension injecting markup, say — poisoning the analysis.

It samples every page view rather than a subset, and it's a **separate beacon from the [Critical CSS](https://xspeedcache.com/docs/critical-css/) fold beacon**. They measure different questions: this one asks "what does this page use at all?", the fold beacon asks "what paints first?"

### Rendering service URL

The problem this solves is the same one the safelist exists for, attacked from the other side. Reading a page's HTML cannot tell you what CSS it uses **after someone clicks**: a class JavaScript adds on interaction is missing from the served markup in exactly the way a class no element ever carries is missing. The two are indistinguishable, so pruning either keeps dead CSS or breaks a working control.

Point this at a rendering service and each page is opened in a real browser and measured for real — including the states it reaches on interaction — before anything is pruned.

**You usually don't need to set it.** With an active licence, generation is brokered through the xSpeed Hub on its own — using your licence, so no Hub connection is required — and the credential that reaches the renderer stays on the Hub instead of on every install. Fill this in only to point at a **self-hosted** renderer, which takes precedence when set. **Rendering service token** is the bearer token for that service, if it needs one.

> 💡 **Leaving it empty is the normal setup.** The Hub brokers rendering for any licensed install, so there's nothing to configure. What changed in this release is failure: a render that fails is now reported and builds pause briefly, rather than the page being quietly pruned from its markup instead — which lost accuracy on interactive states without telling anyone. The safelist below still covers the classes a render can't reach.

### Never prune these selectors

The safelist, and the setting that will save you. It takes **selector fragments, one per line**, matched as **case-insensitive substrings of the whole selector**. So short entries are broad — `cart` also keeps `.woocommerce-cart-form`.

There's already a built-in list covering common runtime states. Use this field for **classes a plugin or theme only adds after your own JavaScript runs** — because static analysis can't see them. A modal that gets `.is-open` on click, a menu that gets `.active` on scroll, a class applied by a third-party widget: all invisible to analysis, all pruned, all broken.

> ⚠️ **This is where problems come from, and they're not obvious.** A page looks fine on load and breaks only when someone interacts — opens a menu, adds to cart, triggers a modal. Test those interactions after enabling, not just the initial render. When something breaks, add its class to the safelist rather than turning the feature off.

---

## A sensible workflow

1. **Turn on Enable Unused CSS.** Pages start building on their own as visitors reach them.
2. **Build your key templates now** instead of waiting — homepage, a post, a page, and anything with interactive UI — then check them in a private window.
3. **Interact with everything** — menus, modals, tabs, carts, accordions. This is the step people skip.
4. **Safelist anything that broke**, then rebuild that page.
5. **Optionally enable real-visitor learning** to refine over time.

---

## Related

- [How to optimize CSS and JavaScript](https://xspeedcache.com/docs/css-and-javascript/)
- [How to generate Critical CSS](https://xspeedcache.com/docs/critical-css/)
- [How to minify CSS and JavaScript](https://xspeedcache.com/docs/minify/)
- [How to activate your licence](https://xspeedcache.com/docs/license/)
