<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Web-Performance · XB Field Notes</title><link>https://hk.xbcatm.com/tags/web-performance/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/web-performance/index.xml" rel="self" type="application/rss+xml"/><item><title>Compression changes the selected HTTP representation</title><link>https://hk.xbcatm.com/notes/compression-changes-the-selected-http-representation/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/compression-changes-the-selected-http-representation/</guid><description>Treat content encoding, validators, and Vary as one observable response contract.</description><content:encoded>&lt;p&gt;Compression saves transfer bytes, but it also creates another representation of a resource. A client announces acceptable content codings with &lt;code&gt;Accept-Encoding&lt;/code&gt;; the server describes the applied coding with &lt;code&gt;Content-Encoding&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/compression.detail.en.svg" alt="A negotiation fork selects identity bytes A or encoded bytes B, requiring different strong tags. Vary controls matching; weak tags have a qualified alternative. A separate pipeline keeps encoded Content-Length in saved response headers while curl decodes the body before an artifact-checksum comparison." width="720" height="2320"&gt;&lt;figcaption&gt;Compression selects representation bytes, so negotiation, cache matching, and validators must agree. Distinct coded bytes need distinct strong tags. Compare the decoded download with the artifact; an encoded Content-Length need not equal the decoded file size.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="make-negotiation-visible"&gt;Make negotiation visible&lt;/h2&gt;
&lt;p&gt;When a response can change according to &lt;code&gt;Accept-Encoding&lt;/code&gt;, include that request field in &lt;code&gt;Vary&lt;/code&gt;. This tells shared caches that a compressed response and an identity response are not interchangeable cache entries.&lt;/p&gt;
&lt;p&gt;Compare the two paths instead of checking only a browser request:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --compressed -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Inspect &lt;code&gt;Content-Encoding&lt;/code&gt;, &lt;code&gt;Vary&lt;/code&gt;, &lt;code&gt;Content-Length&lt;/code&gt; when present, and the validator. Strong entity tags must differ when content coding changes the representation bytes. A shared weak tag is possible for semantically equivalent variants, but it cannot support strong comparison. Check conditionals against the same negotiated representation.&lt;/p&gt;
&lt;h2 id="compress-appropriate-content"&gt;Compress appropriate content&lt;/h2&gt;
&lt;p&gt;Text formats such as HTML, CSS, JSON, XML, and SVG usually benefit from compression. Formats that are already compressed might not. Caddy&amp;rsquo;s &lt;code&gt;encode&lt;/code&gt; directive negotiates supported encodings and normally avoids encoding a response without an appropriate content type or minimum size.&lt;/p&gt;
&lt;p&gt;Do not add compression solely to satisfy a score. Measure a representative response, confirm that the edge emits the intended headers, and verify that downloadable files retain the correct content type and checksum after transfer decoding.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;--compressed&lt;/code&gt;, curl decodes the body automatically but leaves saved response headers unchanged. A reported &lt;code&gt;Content-Length&lt;/code&gt;, when present, describes encoded content, so it need not equal the size of the decoded download. Compare a checksum with the artifact only after content decoding; HTTP transfer framing is a separate layer. The option also does not guarantee compression: inspect the actual response, especially for small or already-compressed files.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;, content codings and &lt;code&gt;Vary&lt;/code&gt;, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/encode"&gt;Caddy &lt;code&gt;encode&lt;/code&gt; directive&lt;/a&gt;, content encoding selection and negotiation, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, &lt;code&gt;--compressed&lt;/code&gt;, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Cache headers are a contract with two clocks</title><link>https://hk.xbcatm.com/notes/cache-headers-are-a-contract-with-two-clocks/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/cache-headers-are-a-contract-with-two-clocks/</guid><description>Choose freshness and revalidation rules from how often a representation can change.</description><content:encoded>&lt;p&gt;Caching is not a single switch. A response has a freshness decision made by a cache and a validation conversation with the origin. Mixing those two ideas produces either stale content or needless requests.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/cache-clocks.detail.en.svg" alt="A storage and reuse matrix is followed by stable and fingerprinted URL policies. A conditional GET sends the real ETag in If-None-Match: a weak match can return 304, while a changed validator normally returns a new 200. Weak equivalence need not mean equal bytes. Age, Vary, freshness-limited immutable, and previously cached lifetimes constrain reuse." width="720" height="2800"&gt;&lt;figcaption&gt;Storage, freshness, and validation answer separate questions. Match the policy to how the URL changes, preserve validators exactly, and check shared-cache variants.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="pick-the-change-model-first"&gt;Pick the change model first&lt;/h2&gt;
&lt;p&gt;For an HTML document that may change after publication, &lt;code&gt;Cache-Control: no-cache&lt;/code&gt; is usually a more accurate starting point than &lt;code&gt;no-store&lt;/code&gt;. &lt;code&gt;no-cache&lt;/code&gt; permits storage but requires a cache to validate before reuse; &lt;code&gt;no-store&lt;/code&gt; asks caches not to store the response at all. The distinction matters when you want conditional requests and a cheap &lt;code&gt;304 Not Modified&lt;/code&gt; response.&lt;/p&gt;
&lt;p&gt;For a fingerprinted asset such as &lt;code&gt;app.4f2c.js&lt;/code&gt;, the URL changes when the bytes change. A long freshness lifetime can then be reasonable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That policy is unsafe for a stable URL whose contents can be replaced in place. If the URL is stable, prefer a shorter lifetime and a validator such as an entity tag (&lt;code&gt;ETag&lt;/code&gt;) or a meaningful &lt;code&gt;Last-Modified&lt;/code&gt; value.&lt;/p&gt;
&lt;p&gt;Choose who may store the response as well as how long it stays fresh. Use the unqualified &lt;code&gt;private&lt;/code&gt; directive when shared caches must not store user-specific content; a browser’s private cache can still store it. &lt;code&gt;no-cache&lt;/code&gt; alone does not provide that restriction. The &lt;code&gt;public&lt;/code&gt; example above is for assets safe to share across users, not a default for personalized responses.&lt;/p&gt;
&lt;h2 id="inspect-both-paths"&gt;Inspect both paths&lt;/h2&gt;
&lt;p&gt;Make one ordinary request and one conditional request. The exact server flags vary, but the evidence you want is stable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s1"&gt;&amp;#39;If-None-Match: &amp;#34;copy-a-real-etag-from-the-first-response&amp;#34;&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do not paste a made-up validator into a runbook. Copy the value from the first response or use a test fixture. For this conditional GET, a matching &lt;code&gt;If-None-Match&lt;/code&gt; can yield &lt;code&gt;304&lt;/code&gt; using weak comparison: it permits semantic equivalence, not necessarily byte identity. A changed validator normally produces a new &lt;code&gt;200&lt;/code&gt; response. Keep the ETag’s quotes and any &lt;code&gt;W/&lt;/code&gt; prefix intact.&lt;/p&gt;
&lt;h2 id="keep-intermediaries-visible"&gt;Keep intermediaries visible&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Age&lt;/code&gt;, &lt;code&gt;Vary&lt;/code&gt;, and the cache-control directives explain why two clients can receive different results. If a response varies by &lt;code&gt;Accept-Encoding&lt;/code&gt;, language, or another request field, that dimension belongs in &lt;code&gt;Vary&lt;/code&gt;; otherwise a shared cache can reuse the wrong representation.&lt;/p&gt;
&lt;p&gt;The useful release check is not “the scanner found a cache header”. It is “the header matches the replacement policy, and a conditional request behaves as documented”.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;immutable&lt;/code&gt; applies during the advertised freshness lifetime; it is not a permanent ban on revalidation. Keep versioned URLs immutable and older assets reachable while old HTML can still reference them. Changing today’s cache header does not retroactively shorten a previously cached response’s lifetime, so a cache-policy mistake may require a new URL or an explicitly supported cache purge.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;, sections 4–5, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;, sections 8 and 13, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control"&gt;MDN: &lt;code&gt;Cache-Control&lt;/code&gt;&lt;/a&gt;, directive semantics and defaults, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8246.html"&gt;RFC 8246 — Immutable Responses&lt;/a&gt;, freshness-limited immutable semantics, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>