# Web Optimizations

> Per-site performance switches — automatic WebP images, Early Hints, JS/CSS minification and the dynamic page cache — applied by the web server without touching the site's code.

Source: https://www.corepanel.net/docs/web/optimizations/
Last updated: 2026-08-13
Part of the CorePanel documentation — https://www.corepanel.net/docs

---

CorePanel can speed up a site at the web-server level, with no plugin, no build step and
no change to the site's code. The switches are **per site**, so you can enable one for a
site and leave the rest alone.

> **Running WordPress?**
>
> This page is the reference for each switch. If the site is WordPress — which is where the
> page cache actually applies — [Speed Optimizations](https://www.corepanel.net/docs/wordpress/speed) is the
> step-by-step guide: what to enable in which order, how to verify it, how to turn it off
> and what each failure looks like.
You will find them in the account workspace's **Optimizations** section, with a site
selector at the top. Every site of the account is listed there and each carries its own
settings:

- the **primary** domain,
- each **addon** domain — an independent site with its own document root,
- each **subdomain** (`blog.example.com`) — likewise a site of its own, usually running
  its own application,
- each **alias** (parked) domain, which serves a copy of another domain's site.

Nothing is inherited between them. Turning the page cache on for `example.com` does not
touch `blog.example.com`, and vice versa — tune each site for what it actually serves.

## What is available

| Optimization | What it does | Editions |
|---|---|---|
| **WebP** | Serves images as WebP to browsers that accept it, keeping the original for those that do not | All, including Personal |
| **Early Hints** | Sends a `103 Early Hints` response with the page's key assets so the browser can start fetching them while the server is still building the page | All, including Personal |
| **Minify JS** | Strips whitespace and comments from JavaScript responses | Speed Optimizer — Pro and above |
| **Minify CSS** | Same for stylesheets | Speed Optimizer — Pro and above |
| **Dynamic page cache** | Serves a whole page from cache so a repeat visitor never waits for PHP | Speed Optimizer — Pro and above |

WebP and Early Hints are **baseline features**: they are part of every edition, including
the free Personal edition, and are not something you upgrade for.

## Three states, not two

WebP, Early Hints and the two minify switches have three states, and the difference
matters (the page cache works differently — see [its own section](#dynamic-page-cache-pro-and-above)):

| State | Meaning |
|---|---|
| **Default** | No per-site setting stored — the server's default applies |
| **On** | Forced on for this site, whatever the server default is |
| **Off** | Forced off for this site, whatever the server default is |

Leaving a switch on *Default* is not the same as turning it off: it means "follow the
server". Set it explicitly only when this site must differ.

## WebP

Modern browsers advertise WebP support; older ones do not. With this on, an image request
from a browser that accepts WebP is answered with a WebP version, while everything else
keeps receiving the original file. The files in your document root are untouched.

This is usually the single largest win on an image-heavy site, and it requires nothing
from the application.

How much it saves is decided by what was uploaded, not by the server, and the range is
wide enough to be worth knowing before you measure:

| The page | Weight, WebP off | WebP on | Time to load |
|---|---|---|---|
| Photographs at sensible sizes | 754 KB | 699 KB | unchanged (1.78 s → 1.79 s) |
| Six 2–3.5 MB PNGs displayed 480 px wide | 14.3 MB | **75 KB** | **4.9 s → 1.0 s** |

Both rows are the same site on the same server, measured the same day. Flat graphics —
screenshots, logos, charts, exports — are what PNG handles worst and WebP handles best: a
3.47 MB PNG in that second page came down to 11 KB. Photographs that were already exported
sensibly give up far less, because there was less to give.

## Early Hints

A normal page load is serial: the browser asks for the HTML, waits while the server (and
PHP, and the database) build it, and only then discovers which CSS and JS it needs.

`103 Early Hints` breaks that wait. CorePanel learns which assets a page pulls in and, on
subsequent requests, sends them as hints before the real response — so the browser
fetches the stylesheet while PHP is still working.

> **Where Early Hints apply**
>
> Hints are emitted for responses that go through PHP-FPM or a proxied backend — the ones
> that actually take time to generate. A purely static file is already fast and is served
> without them.
## Minification (Pro and above)

Minify JS and Minify CSS shrink those responses on the way out. They are part of the
**Speed Optimizer** feature set of the Pro edition, alongside dynamic caching.

Minification is generally safe, but it is a transformation of what your application
returns. If a site depends on the exact bytes of its scripts — an integrity hash, an
inline source map — verify after enabling.

## Dynamic page cache (Pro and above)

Every other optimization on this page makes a page **cheaper** to produce. This one skips
producing it. On a cache hit the web server answers the request itself: PHP is never
started, WordPress never boots, no plugin runs and the database is never queried. The page
goes out through the same short path a static file takes — which is why this is the largest
speed-up available to a WordPress site, and the one that holds up when traffic arrives all
at once.

What normally makes a page cache a gamble is freshness: cache too long and visitors read
yesterday's site, cache too briefly and you are back to building every page. CorePanel does
not trade one against the other, because the application itself drives the cache.

### WordPress declares what is shareable, page by page

A companion plugin runs inside WordPress and, on every response, tells the server three
things: whether this page may be shared with other visitors, for how long, and which URLs
have just gone stale. Nothing is cached that was not explicitly declared shareable — there
are no URL patterns to guess and no exclusion list for you to maintain.

The plugin declines to share a page when the visitor is logged in, when a cart holds
anything, when the response sets a cookie of its own, and in a handful of similar cases.
Where it can, it solves the problem instead of declining: a page whose form carries a
one-time security token stays cacheable, but its lifetime is capped a minute short of the
token's own — so the cached copy always hands out a token that still verifies, rather than
a form that silently fails to submit on the second day.

The plugin is installed and kept up to date automatically, in the WordPress sites of **that
site only**: switching `example.com` on leaves a WordPress under `blog.example.com` alone.
There is nothing to configure, and it should not be edited — it is a managed file and the
panel overwrites it.

### It expires when you publish, not when a timer runs out

Pages are stored with a lifetime of up to a year. That is only safe because freshness comes
from invalidation rather than expiry, and WordPress is the one signalling it:

- **Publishing, editing, unpublishing or deleting a post** drops its permalink, the front
  page, its term archives, its post type archive, the feed and the sitemaps.
- **A new, edited, approved or deleted comment** drops the page it lives on.
- **Changes that touch the whole site** — switching theme, saving the Customizer,
  activating or deactivating a plugin, editing a menu, changing the permalink structure —
  drop everything.
- **Updating WordPress, a plugin or a theme** drops everything too, wherever the update
  comes from: the panel's WordPress section, wp-admin, or WordPress's own automatic
  update. Rolling a site back to a restore point does the same.

The invalidation travels on the very response that made the change, including the admin
`POST` that is never itself cacheable, and covers both the `www` and non-`www` form of the
site. Publish, reload, and the new page is already there.

Updates made from the panel have no such response to travel on — they run as commands,
not as page loads — so the panel empties that site's cache itself once the update
succeeds.

Updating **WordPress itself** is the one case where the signal cannot ride the update's own
page: by the time the upgrade finishes, that page has already started printing. It is not
lost — it is carried by the next screen you open in wp-admin, which is normally the very
next click. An automatic update run from cron has no page at all, so the site fetches one
of its own to carry it, and the cache is emptied within the same run.

### What decides the hit rate

- **Tracking parameters do not fragment the cache.** `/post`, `/post?fbclid=…` and
  `/post?utm_source=…` are one entry, with the remaining query sorted so the order a link
  was written in does not matter. A campaign or a newsletter therefore lands on a cached
  page instead of building a fresh copy per visitor — and a later purge of `/post` finds
  that entry.
- **A page under load is built once.** When several visitors ask for the same uncached
  page at the same time, one request goes to PHP and the rest wait for its result. Traffic
  spikes do not turn into a pile-up of identical renders.
- **Conditional requests never touch the stored body.** A browser revalidating with
  `If-None-Match` gets its `304` straight from the cache metadata.

> **Cookie consent banners**
>
> A banner that decides *in PHP* what the page contains — the tracking tags, a class on
> `<body>` — makes one URL into two different pages, and only one of them is stored. The
> companion plugin recognises **Cookie Notice & Compliance** and **CookieYes in its legacy
> mode** and stops caching those sites, because the alternative is serving tracking tags to
> visitors who refused them. **Complianz**, **Real Cookie Banner** and **CookieYes** on its
> web app block in the browser instead, and cache normally.
>
> Any other banner, and themes that render their own `if ( $_COOKIE[…] )`, are worth checking:
> [the one-minute test is in the companion plugin's page](https://www.corepanel.net/docs/wordpress/security/#the-cache-module).
### Cookies that mean "this visitor is different"

Some cookies make a page personal, and a personal page must never be shared. CorePanel
already knows the ones that matter on a normal WordPress: the login session, the comment
author fields, the WooCommerce cart and session, and the cookies the **Aelia Currency
Switcher** sets. A request carrying any of them skips the cache entirely, and its response
is never stored. You do not configure that, and you cannot turn it off.

What CorePanel cannot know is the cookie *your* plugin invented. A country switcher, a
per-visitor price list, a members-only banner, a B2B catalogue — anything that reads a
cookie in PHP and renders a different page because of it. The symptom is unmistakable when
it happens: **a visitor sees something that belongs to another visitor**. Prices in the
wrong currency, a greeting with somebody else's name, the members' block on an anonymous
page.

Add the cookie's name to the site and the problem stops immediately:

```bash
corepanel domain optimizations set shop.example.com --state-cookies "my_currency,my_shop_*"
```

Names are comma-separated, a trailing `*` matches a prefix, and at most 32 fit. They are
**added to** the ones CorePanel already knows — nothing you type here can switch those off.

Two things worth knowing:

- **Both services must be up to date.** The names are written to the site by
  `corepanel-sys`. If you upgraded `corepanel-core` on its own, the command refuses with
  *"corepanel-sys is older than corepanel-core"* instead of accepting a list it cannot
  apply — a cookie the panel lists but the server never received would be worse than no
  cookie at all. Upgrade `corepanel-sys` and set them again.
- **The list survives everything else.** Unlike the other flags, omitting `--state-cookies`
  keeps what the site already has, so changing the WebP setting later does not quietly
  delete it. Clear it deliberately with `--state-cookies ""`.
- **Every request carrying that cookie now costs full PHP.** That is the point — those
  visitors were getting a wrong page and now get a correct one — but if the cookie is set
  for *everyone* on arrival, the site effectively stops being cached. If the hit rate
  collapses after adding a name, that is what happened, and the fix belongs in the plugin
  (set the cookie only when the visitor actually chooses something).
- **Restoring an old backup clears it.** An archive made before this setting existed does
  not carry the names, and a restore sets the site to what the archive says. The restore
  report warns you and lists what it removed, so add them back afterwards.

> **WordPress only, for now**
>
> A site that is not WordPress is not cached at all. Without an application that can say what
> is shareable and which URLs to invalidate, the honest answer is not to cache — an old page
> served to a customer is worse than a slow one.
> **Not on alias domains**
>
> An **alias** (parked) domain serves a copy of another domain's site, and WordPress
> invalidates by path — it never learns which hostname a visitor arrived through. A cached
> alias would therefore keep serving the old page after you publish, for as long as that
> page's lifetime, so CorePanel refuses to enable the cache on one. Enable it on the domain
> the alias points to. The other optimizations on this page have no such problem and work on
> an alias normally.
### What it measures out at

Numbers from one run on a deliberately modest server — **2 vCPU, 3.5 GB RAM, AlmaLinux 8**
— serving a WordPress 7.0 site with 400 posts, 2,488 comments and a 500 MB media library
(the [`wp-test`](https://github.com/pyxsoft/wp-test) `medium` fixture, PHP 8.4 with
OPcache on, MariaDB 10.11). Each figure is the median of 40 requests per page.

![Bar chart of time to first byte with and without the page cache, on a WordPress site with 400 posts on a 2-vCPU server: the home page goes from 248 ms to 1.6 ms, a post permalink from 194 ms to 2.0 ms, a category archive from 270 ms to 1.1 ms, while search results stay at 289 ms and 291 ms because they are never cached.](https://www.corepanel.net/_astro/dcache-ttfb-dark.CAiFjx9k.svg)

| Page | Time to first byte, cache off | Cache on |
|---|---|---|
| Home page | 248 ms | **1.6 ms** |
| Post permalink | 194 ms | **2.0 ms** |
| Category archive | 270 ms | **1.1 ms** |
| Search results (never cached) | 289 ms | 291 ms |

The second chart is the one that decides how much traffic a server takes, because it is
what the machine actually spends: on a hit there is no PHP process, no database query and
no plugin code, so a page view costs about a millisecond of CPU instead of a quarter of a
second.

![Bar chart of server CPU per page view, counting PHP-FPM, MariaDB and the web server together: the home page falls from 266 ms to 1.0 ms of CPU, a post permalink from 197 ms to 1.5 ms, a category archive from 287 ms to 0.8 ms, while search results stay at 309 ms and 312 ms.](https://www.corepanel.net/_astro/dcache-cpu-dark.7JivKv-p.svg)

That is what turns into capacity. Requested from a second machine 26 ms away, the same
post permalink:

![Bar chart of pages served per second under concurrent load: with 20 visitors at once the server serves 11.6 pages per second without the cache and 701 with it, and with 400 visitors at once 12.1 without the cache and 5,507 with it.](https://www.corepanel.net/_astro/dcache-throughput-dark.B37L6IPy.svg)

| Concurrent visitors | Cache off | Cache on |
|---|---|---|
| 20 | 11.6 pages/s · p95 2.0 s | **701 pages/s · p95 51 ms** |
| 400 | 12.1 pages/s · p50 26 s | **5,507 pages/s · p95 124 ms** |

Two honest caveats. Without the cache the server is already at 100% CPU with 20 visitors,
so the extra concurrency buys nothing but a longer queue — that is why the two rows agree.
And with the cache on, 20 concurrent visitors do not measure the server at all: a cached
page costs one round trip, so 701 pages/s is just 20 requests divided by the 26.7 ms the
link itself takes. Only at 400 concurrent visitors does the box run out of cores.

Measured on **corehttpd 0.7.8**. Earlier builds served cached responses with
`Connection: close`, which cost a TCP — and over TLS a full — handshake per cached page;
the figures above are 2.8× (20 visitors) and 1.6× (400) what the same machine did before
that fix. If you are still on 0.7.7 or older, update before benchmarking.

A note if you benchmark this yourself: have the load generator request gzip and *not*
decompress it. Our first run reported 1,512 pages/s because the client was spending its
own cores on 164 MB/s of gunzip — the same server, measured without that, serves 5,507.

Your own site will land somewhere else — the ratio follows how heavy your PHP is and how
much of your traffic is cacheable, which is exactly what *Evaluate* below is for.

#### The same measurement on a page builder

A second run on the same server, with the [`wp-test`](https://github.com/pyxsoft/wp-test)
`agency` fixture — Elementor and seven more plugins, 30 builder pages carrying 2,399
widgets — moves every figure in the same direction, further:

| | Cache off | Cache on |
|---|---|---|
| Home page, time to first byte | 511 ms | **0.9 ms** |
| Home page, CPU per view | 516 ms | **0.7 ms** |
| 20 concurrent visitors | 4.4 pages/s · p50 4.2 s | **679 pages/s · p50 27 ms** |
| 400 concurrent visitors | 251 pages in 20 s, **231 requests timed out** | **78,946 pages in 20 s**, none refused |

A builder page costs more to produce than an ordinary post — half a second rather than a
quarter — so the ratio is larger, and the ceiling without the cache is lower: four pages
a second, which a modest campaign exhausts. On the cached side the two fixtures agree,
because a hit is a hit whatever built it. Full method and charts in
[the write-up](https://www.corepanel.net/blog/elementor-cache-webp-http3).

### Evaluate before you serve

The page cache has three modes, not an on/off switch:

| Mode | What happens |
|---|---|
| **Off** | No page caching. |
| **Evaluate** | The cache decides and stores exactly as it would in production, and **serves none of it**. Visitors get the same site they got yesterday. |
| **On** | Pages are served from cache. |

*Evaluate* exists because how much of a site is cacheable cannot be guessed from the
outside. A news site and a membership site with identical traffic land in completely
different places, and the difference is not visible until you measure it. Leave a site in
Evaluate for a few days, then switch it on.

Leaving Evaluate is clean: everything stored while evaluating is discarded, so nothing
built under rules you were still testing is ever served. The only cost is that those
pages are generated once more.

### The evaluation report

While a site is in *Evaluate*, the Optimizations section shows what the cache is finding:

- **How much of the cacheable traffic would be served from cache.** This is the number
  that says whether the cache is worth switching on.
- **How much of all traffic could never be cached**, as a separate figure. The two are
  kept apart on purpose: folding them together would produce one number that answers
  neither question. A shop where every visitor carries a cart is not a site with a bad
  cache — it is a site the cache cannot help.
- **Why requests were not cacheable**, by reason. `state-cookie` means visitors are
  logged in or carrying a cart; `no-contract` means the companion plugin is not answering,
  which on a WordPress site usually means it is not installed there; `set-cookie` means
  the page issues a cookie of its own.
- **Which `Vary` header blocked**, when that is the reason. This is the difference between
  a finding and a dead end: a language or encoding header can be handled with a cache
  dimension, while one that varies per visitor (`Cookie`) cannot be handled at all.

> **The report only exists while the site is evaluating**
>
> Counting runs on every response, so it is something a site does during a measurement
> window, not forever. **Switching the cache on stops the measurement** — the report is
> there to help you make that decision, not to monitor the cache afterwards. The counters
> also start over whenever the web server restarts, so a figure never mixes two different
> configurations.
### Emptying the cache by hand

WordPress refreshes its own cached pages as content changes, so day to day there is
nothing to do. What it cannot signal is a change it never saw:

- a theme or a template edited over SFTP or in a file manager,
- a database restored underneath the site,
- a page that was cached while something was already wrong.

For those, **Empty cache** in the Optimizations section drops every stored page for the
selected site. The button appears only while the cache is *On* — under *Evaluate* nothing
is being served, and switching that mode off already discards what it stored.

```bash
corepanel cache purge example.com                # the whole site
corepanel cache purge blog.example.com / /feed   # just those pages
corepanel cache purge example.com "/2026/*"      # everything under a prefix
```

Two things are worth knowing:

- **Every hostname of the site is emptied, not just the one you named.** A page is stored
  once per hostname a visitor arrived through, so `example.com` and `www.example.com` hold
  separate copies. The panel and the CLI both report the hostnames they reached — if one
  you expected is missing, it is a separate site with its own cache.
- **Emptying the cache is never blocked**, even on a server whose licence has lapsed. The
  cache is a Pro feature, but pages cached while it was licensed are still on disk, and
  being unable to clear a wrong page is not an acceptable way to be reminded of a renewal.

Clearing the cache costs the next visitor to each page one normal, uncached page load.
There is no other side effect.

### What to expect

- **A logged-in visitor is never served a cached page.** Your customers editing their
  site see exactly what they would have seen without the cache.
- **The cache does not shorten how long your content is wrong.** It shortens how long
  each page takes. When you publish, the affected URLs are dropped immediately.
- **A store or a membership site will see a smaller win** than a blog, because far more
  of its traffic is personal to each visitor. That is the cache working correctly, not
  failing.

## Browser cache: how long a visitor keeps a file

Everything above is about what the server sends. This is about what the visitor's browser
keeps, so it does not ask again — and it needs no setting, because CorePanel decides it
per file type.

| Files | Kept for |
|---|---|
| Anything requested with `?ver=` | 1 year, `immutable` |
| Fonts — `woff2`, `woff`, `ttf`, `otf`, `eot` | 1 year |
| Images, audio and video | 30 days |
| CSS, JavaScript, PDF | 7 days |
| HTML | 1 hour |

The scale comes from one question: **when you replace this file in place, under the same
name, how long does a visitor keep seeing the old one?** Nobody can clear a browser cache
— not you, not us, not support — so each row is a promise that cannot be taken back early.

That is why the rows are not all a year, which is what most speed tools ask for. A
typeface is never edited in place, so it costs nothing. A stylesheet is edited often and a
stale one is a visibly broken page, so it gets a week. And HTML stays at an hour because a
stale page is the failure a visitor actually notices — pages are handled by the [page
cache](#dynamic-page-cache-pro-and-above) instead, on the server, where publishing an edit
can genuinely clear it.

**If your site versions its assets, you already have the year.** WordPress does this out
of the box (`style.css?ver=6.4`), and so does every modern build tool. Any URL carrying
`?ver=` is served with a year and `immutable`, which tells the browser not even to
revalidate: the URL changes when the file does, so there is nothing to go stale.

**Changed a file and still see the old one?** Add or bump a `?ver=` on it — that is the
mechanism, and it works immediately for every visitor, including the ones already holding
a copy. Renaming the file does the same.

An `.htaccess` asking for a different policy is not applied; see [the compatibility
report](https://www.corepanel.net/docs/web/htaccess#what-not-applied-means-for-caching-and-headers) for why.

## A sensible order to enable things

1. **WebP** — biggest win, lowest risk, free.
2. **Early Hints** — helps every dynamic page, free.
3. **Minify CSS**, then **Minify JS** — check the site after each.
4. **Dynamic page cache** — in *Evaluate* first, then on. It is last because it is the
   one that changes what a visitor receives, and the first three are already in place to
   compare against.

Measure before and after on a real page rather than trusting a single synthetic score;
the [Web Traffic graphs](https://www.corepanel.net/docs/web-traffic) show what your server is actually serving.
