Pagespeed: verifying caching and compression
STATUS: SEEDED, NOT COMPLETE. Covers only caching and compression verification.
Missing: Core Web Vitals, LCP, render-blocking resources, image strategy, preload hints,
Lighthouse workflow, bundled audit script. Absence of a topic here means "not written yet".
Why this exists
The facts below are not obscure. The failure mode is not knowing them, it is not running the
check, and then believing a clean result that was never capable of being dirty. Both defects
here are silent: no error, no warning, just bytes leaving again.
Run these checks
Check at the hop the visitor reaches (a CDN or reverse proxy can add, strip, or override both).
# 1. What headers does this asset ACTUALLY carry? GET, never HEAD - see below.
curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip' https://host/path/app.js
# 2. Is it really compressed? Compare bytes on the wire, do not trust the header alone.
curl -s -H 'Accept-Encoding: gzip' https://host/path/app.js | wc -c
curl -s https://host/path/app.js | wc -c
# 3. Does revalidation actually cost nothing? A no-cache asset needs a validator,
# or every "revalidation" is a full 200 with the whole body.
curl -s -o /dev/null -D - https://host/path/app.js | grep -iE 'etag|last-modified'
What a false clean looks like
| Check |
Why it can read clean while broken |
curl -I for compression |
Plain curl -I sends no Accept-Encoding, so nothing is negotiated and Content-Encoding is absent whatever the server does. With --compressed a HEAD usually DOES report it: measured 2026-08-28 with curl 8.18.0, HEAD and GET agreed on example.com, wikipedia.org, github.com, cloudflare.com and developer.mozilla.org (br or gzip on both), while python.org compressed nothing on either method even with an explicit Accept-Encoding: gzip. A HEAD can still disagree with a GET where a filter only runs on a body, which this sample did not produce - so confirm with a GET |
Cache-Control: no-cache present |
no-cache means revalidate, not do-not-store. Cheap only if the origin emits ETag/Last-Modified; a proxied app that sends neither turns each revalidation into a full 200 |
| A location "has no cache policy" |
With no add_header of its own it INHERITS the server-level one, often no-cache. Absence of config is a policy |
| A later regex location for assets |
A ^~ prefix match stops location selection; regex locations are never evaluated for that prefix |
gzip_types names the type |
Matching is literal against the response Content-Type. application/javascript does not match text/javascript, which is what modern servers send |
Decision rules
immutable requires a URL that changes with its content. Hashed or versioned filename, or
no immutable. On a fixed URL it means stale until expiry with no way to push a fix. Until the
build fingerprints the filename, use a bounded max-age.
add_header inheritance cuts both ways. A location with its own add_header drops all
inherited ones. A location with none inherits the whole server-level set. Adding one header to a
location silently removes the rest there, which is also how security headers get lost; see
bitranox:sec-appsec-web-baseline.
robots and bandwidth are different problems. If the cost is crawler volume rather than repeat
downloads, caching will not fix it; see bitranox:web-seo-crawl-indexing.
Common mistakes
| Mistake |
Reality |
curl -I to test compression |
False negative on every file; use GET and compare wire bytes |
Reading no-cache as "not cached" |
It is revalidate-every-time; without validators that is a full 200 |
immutable on an unhashed path |
Stale until expiry, unfixable |
| Assuming a proxied location has no policy |
It inherits the server-level Cache-Control |
Adding one add_header to a location |
Drops every inherited header there |
Only application/javascript in gzip_types |
Servers send text/javascript; nothing compresses |
| Grading the origin behind a CDN |
Measure where visitors actually land |
Scope boundary
| Concern |
Skill |
| Security headers, CSP, HSTS, cookies |
bitranox:sec-appsec-web-baseline |
| Layout, viewport, tap targets, RTL |
bitranox:web-frontend-responsive-ux |
| robots.txt, crawl budget, indexing |
bitranox:web-seo-crawl-indexing |
1---2name: web-frontend-pagespeed3description: Use when verifying or fixing how a site caches and compresses its assets - bandwidth or CDN cost up with flat request volume, a bundle refetched on every navigation, an asset that will not pick up a new deploy, compression that looks enabled but is not, or a header check that came back clean and you need to know whether to believe it. Keywords - Cache-Control, max-age, immutable, ETag, 304, revalidation, gzip, Content-Encoding, gzip_types, add_header inheritance.4---56# Pagespeed: verifying caching and compression78> **STATUS: SEEDED, NOT COMPLETE.** Covers only caching and compression verification.9> Missing: Core Web Vitals, LCP, render-blocking resources, image strategy, preload hints,10> Lighthouse workflow, bundled audit script. Absence of a topic here means "not written yet".1112## Why this exists1314The facts below are not obscure. The failure mode is not knowing them, it is **not running the15check**, and then **believing a clean result that was never capable of being dirty**. Both defects16here are silent: no error, no warning, just bytes leaving again.1718## Run these checks1920Check at the hop the visitor reaches (a CDN or reverse proxy can add, strip, or override both).2122```bash23# 1. What headers does this asset ACTUALLY carry? GET, never HEAD - see below.24curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip' https://host/path/app.js2526# 2. Is it really compressed? Compare bytes on the wire, do not trust the header alone.27curl -s -H 'Accept-Encoding: gzip' https://host/path/app.js | wc -c28curl -s https://host/path/app.js | wc -c2930# 3. Does revalidation actually cost nothing? A no-cache asset needs a validator,31# or every "revalidation" is a full 200 with the whole body.32curl -s -o /dev/null -D - https://host/path/app.js | grep -iE 'etag|last-modified'33```3435## What a false clean looks like3637| Check | Why it can read clean while broken |38|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|39| `curl -I` for compression | Plain `curl -I` sends no `Accept-Encoding`, so nothing is negotiated and `Content-Encoding` is absent whatever the server does. With `--compressed` a HEAD usually DOES report it: measured 2026-08-28 with curl 8.18.0, HEAD and GET agreed on example.com, wikipedia.org, github.com, cloudflare.com and developer.mozilla.org (br or gzip on both), while python.org compressed nothing on either method even with an explicit `Accept-Encoding: gzip`. A HEAD can still disagree with a GET where a filter only runs on a body, which this sample did not produce - so confirm with a GET |40| `Cache-Control: no-cache` present | `no-cache` means revalidate, not do-not-store. Cheap only if the origin emits `ETag`/`Last-Modified`; a proxied app that sends neither turns each revalidation into a full 200 |41| A location "has no cache policy" | With no `add_header` of its own it INHERITS the server-level one, often `no-cache`. Absence of config is a policy |42| A later regex location for assets | A `^~` prefix match stops location selection; regex locations are never evaluated for that prefix |43| `gzip_types` names the type | Matching is literal against the response `Content-Type`. `application/javascript` does not match `text/javascript`, which is what modern servers send |4445## Decision rules4647**`immutable` requires a URL that changes with its content.** Hashed or versioned filename, or48no `immutable`. On a fixed URL it means stale until expiry with no way to push a fix. Until the49build fingerprints the filename, use a bounded `max-age`.5051**`add_header` inheritance cuts both ways.** A location with its own `add_header` drops all52inherited ones. A location with none inherits the whole server-level set. Adding one header to a53location silently removes the rest there, which is also how security headers get lost; see54`bitranox:sec-appsec-web-baseline`.5556**robots and bandwidth are different problems.** If the cost is crawler volume rather than repeat57downloads, caching will not fix it; see `bitranox:web-seo-crawl-indexing`.5859## Common mistakes6061| Mistake | Reality |62|---------------------------------------------|--------------------------------------------------------------------|63| `curl -I` to test compression | False negative on every file; use GET and compare wire bytes |64| Reading `no-cache` as "not cached" | It is revalidate-every-time; without validators that is a full 200 |65| `immutable` on an unhashed path | Stale until expiry, unfixable |66| Assuming a proxied location has no policy | It inherits the server-level `Cache-Control` |67| Adding one `add_header` to a location | Drops every inherited header there |68| Only `application/javascript` in gzip_types | Servers send `text/javascript`; nothing compresses |69| Grading the origin behind a CDN | Measure where visitors actually land |7071## Scope boundary7273| Concern | Skill |74|--------------------------------------|---------------------------------------|75| Security headers, CSP, HSTS, cookies | `bitranox:sec-appsec-web-baseline` |76| Layout, viewport, tap targets, RTL | `bitranox:web-frontend-responsive-ux` |77| robots.txt, crawl budget, indexing | `bitranox:web-seo-crawl-indexing` |