Performance
The 45 KB you pay for on every single visit
Compression, caching and headers live in the server configuration, not in your site. No optimisation plugin can see them, and one switch left off costs you bytes on every page view.
- Published
- 10 September 2026
- Reading time
- 7 min
I was measuring the front page of my own site last week. The build was clean: stylesheets split per section, nothing render-blocking below the fold, fonts self-hosted and preloaded. On a throttled mobile connection the largest element painted in 997 milliseconds. I was ready to call it finished.
Then I looked at the transfer size. The HTML document was going over the wire at roughly 64 KB. Compressed, that same document is about 19 KB. Forty-five kilobytes, on every single page view, for nothing — because a single Apache module was not switched on.
No plugin would have told me. No Lighthouse score I had been staring at made it obvious. It is one of the most common and least visible ways a fast site is made slow, and it happens in a layer most people never look at.
Where your site actually ends
There is a boundary that matters more than most people realise. Your site — the theme, the plugins, the database, the PHP — produces a response. Everything after that point belongs to the web server: Apache or nginx. Compression, cache headers, security headers, redirects, what happens to a request for a file that does not exist.
This matters because of a simple asymmetry. Everything on your side of the boundary is visible to you. You can open the admin, install a plugin, see a setting. Everything on the server side is invisible unless you deliberately go and look, and almost nobody does. The result is a site that is well built and badly delivered.
I see three of these in nearly every audit.
One: compression is off, or off for the files that matter
Text compresses extraordinarily well. HTML, CSS, JavaScript, JSON and SVG are highly repetitive, and gzip typically takes them to a quarter or a third of their original size. Brotli does slightly better again.
The rule in Apache is not complicated:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
Note what is not in that list. JPEG, PNG, WebP and WOFF2 are already compressed internally. Running them through gzip again costs processor time on every request and saves nothing — occasionally it makes the file marginally larger. A configuration that compresses everything is a configuration written by someone who did not think about it.
The check takes one command:
curl -sI -H "Accept-Encoding: gzip" https://yoursite.com/ | grep -i content-encoding
If nothing comes back, you are sending every visitor three or four times more bytes than you need to. On a fast office connection you will never notice. On a phone with two bars, that is the difference between a site that appears and a site somebody abandons.
And here is the part that caught me: the module was present, the rule was written, the configuration was correct. The running server had simply never been restarted after the module was enabled, so it was serving with the old configuration loaded. The fix was not a code change. It was a restart.
Two: static files are not cached, or are cached wrongly
Your logo does not change. Neither does your stylesheet, between deployments. Yet a browser will re-request them on every visit unless the server tells it not to.
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
A year sounds reckless until you look at the last line. HTML is never cached, because HTML is what your team edits. Everything else gets a long life on the condition that its address changes when the file changes — which is what the ?ver= parameter on a WordPress stylesheet is for. If that version number is derived from the file’s modification time, a deployment automatically invalidates the cache. If it is hardcoded, it does not, and you will spend a support call telling a client to press Ctrl+F5.
That single detail — version by modification time, not by a number somebody remembers to bump — is the difference between long caching being safe and long caching being a trap.
Three: security headers that are not there, or are there twice
Three headers cost nothing and prevent whole categories of attack:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
The first stops a browser guessing that your uploaded text file is really JavaScript. The second stops your site being loaded invisibly inside somebody else’s page. The third stops your internal URLs leaking to every site your visitors click through to.
Here is a subtlety that cost me an hour of my own time, and which I have never seen written down clearly. If your theme sets those headers in PHP and your server sets them in configuration, you can end up sending each one twice. The reason is that Apache keeps two separate tables of response headers. Header always set writes into the table used for error responses; PHP writes into the table used for successful ones. Because they are different tables, setifempty does not see the PHP value and adds its own. The response goes out with two copies of everything.
The fix is not clever, it is just deliberate: decide which layer owns which response. In my case the theme handles HTML documents and the server handles static files, which is exactly the split you want — because PHP never runs for a stylesheet, and a stylesheet needs those headers too.
What this is worth in real money
Let us be concrete rather than dramatic. Forty-five kilobytes per view.
A modest business site doing 10,000 page views a month is shipping 450 MB of pure waste. That is not a hosting bill anyone will notice. The cost is somewhere else entirely: it is in the time before your customer sees anything, on the connection they actually have, in the moment where they decide whether this is going to work or whether they should go back to the search results.
Google has been explicit that this affects ranking, but I find that argument less persuasive than the direct one. A visitor on a train with one bar of signal is not thinking about Core Web Vitals. They are deciding, in about two seconds, whether your business seems competent.
How to check your own site in five minutes
- Compression:
curl -sI -H "Accept-Encoding: gzip" https://yoursite.com/ | grep -i content-encoding— you wantgziporbr. - Caching: request a stylesheet directly and look for
Cache-Controlwith a longmax-age. - Headers: request the home page, then request a CSS file. Both should carry the three headers, each exactly once.
- Then look at the transfer size of the HTML document in your browser’s network panel. If it is over about 30 KB, compression is probably not applying.
None of this requires a plugin. Two of the three cannot be fixed by a plugin at all, because they happen after PHP has finished running.
The point underneath
Site speed is usually discussed as though it were a property of the site. It is not. It is a property of the whole delivery path: the code, the server configuration, the network, and the device at the far end. Optimise one layer and ignore the others and you get exactly what I had — a page that painted in under a second and still shipped three times more bytes than it needed to.
The uncomfortable part is that the invisible layer is often the cheapest to fix. Four lines of configuration and a restart. But nobody fixes what nobody measures, and almost nobody measures the layer they do not own.