Cache-Control vs. ETag: What Each One Actually Controls
Cache-Control vs. ETag: What Each One Actually Controls
Cache-Control 与 ETag:它们各自究竟控制了什么
Open a browser’s network tab and you’ll see Cache-Control and ETag sitting on the response headers of nearly every image, stylesheet, and script. Most developers recognize both names, but far fewer could explain how they actually divide the work of caching between them. This post walks through the basics of HTTP caching, then looks at how this project’s own OGP image generator uses one of these headers and deliberately skips the other.
打开浏览器的网络选项卡,你会看到 Cache-Control 和 ETag 出现在几乎每一张图片、样式表和脚本的响应头中。大多数开发者都认识这两个名字,但很少有人能解释它们是如何分担缓存工作的。本文将介绍 HTTP 缓存的基础知识,并探讨本项目自身的 OGP 图片生成器是如何使用其中一个响应头,并刻意跳过另一个的。
The Problem These Headers Solve
这些响应头解决的问题
HTTP caching lets a browser (or a CDN in between) hold on to a response it already fetched — an image, a stylesheet, a script — and reuse it on the next request for the same URL instead of asking the server again. Without any instructions from the server, a browser has to guess whether a cached response is still safe to reuse, and that guess tends to be inconsistent across browsers and situations. Cache-Control and ETag exist to remove the guesswork by having the server state its intent explicitly. But they answer two different questions: Cache-Control answers “how long can you skip asking me entirely?” and ETag answers “once that period is over, how can you cheaply check whether anything actually changed?”
HTTP 缓存允许浏览器(或中间的 CDN)保留已获取的响应(如图片、样式表或脚本),并在下次请求相同 URL 时直接复用,而无需再次询问服务器。如果没有服务器的指令,浏览器必须猜测缓存的响应是否仍然安全可用,而这种猜测在不同浏览器和场景下往往是不一致的。Cache-Control 和 ETag 的存在就是为了消除这种猜测,让服务器明确表达其意图。但它们回答的是两个不同的问题:Cache-Control 回答的是“你可以完全不询问我多久?”,而 ETag 回答的是“当这段时间结束后,如何以低成本检查内容是否真的发生了变化?”
What Cache-Control Controls
Cache-Control 控制什么
Cache-Control sets an expiration window. This project’s blog theme includes ogp-generator.php, which renders an OGP preview image with PHP’s GD library for any post that has no featured image set. When it serves the generated PNG, it attaches:
Cache-Control 设置了一个过期窗口。本项目的博客主题包含 ogp-generator.php,它会使用 PHP 的 GD 库为任何未设置特色图片的文章生成 OGP 预览图。当它提供生成的 PNG 图片时,会附带以下响应头:
header('Content-Type: image/png');
header('Content-Length: ' . filesize($file));
header('Cache-Control: public, max-age=2592000, immutable');
Each directive does something distinct:
- public — this response can be cached not just by the requesting browser but by any shared cache along the way, such as a CDN. (A response with per-user content, like a personal dashboard, would use private instead.)
- max-age=2592000 — the number of seconds the cache is considered fresh. 2,592,000 seconds is exactly 30 days; for that entire window, a browser revisiting the same URL never contacts the server at all.
- immutable — a declaration that the content will not change for the duration of max-age. With this set, the browser skips even the lightweight revalidation check described below on a page reload — it just uses the cached copy, no questions asked.
每个指令都有其独特的作用:
- public — 该响应不仅可以被请求的浏览器缓存,还可以被沿途的任何共享缓存(如 CDN)缓存。(包含用户个人内容(如个人仪表盘)的响应则应使用 private。)
- max-age=2592000 — 缓存被视为“新鲜”的秒数。2,592,000 秒正好是 30 天;在整个窗口期内,浏览器再次访问同一 URL 时根本不会联系服务器。
- immutable — 声明内容在 max-age 期间不会发生变化。设置此项后,浏览器在页面重新加载时甚至会跳过下文描述的轻量级重新验证检查——它会直接使用缓存副本,无需任何询问。
Pairing a 30-day window with immutable only makes sense if “this exact URL will never point to different content” genuinely holds. How that guarantee gets built is the interesting part.
将 30 天的窗口期与 immutable 结合使用,前提必须是“这个特定的 URL 永远不会指向不同的内容”这一假设确实成立。如何构建这种保证才是最有趣的部分。
What ETag Controls — Checking In After Expiration
ETag 控制什么——过期后的检查
Once max-age expires, a browser doesn’t simply throw the cached copy away — it asks the server whether the old copy is still good. ETag (short for Entity Tag) is what makes that check cheap. An ETag is a short identifier — usually a hash — generated from a response’s actual content. If even a single byte of the content changes, the ETag changes with it, making it effectively a fingerprint of the content.
一旦 max-age 过期,浏览器不会简单地丢弃缓存副本,而是会询问服务器旧副本是否仍然有效。ETag(实体标签的缩写)正是让这种检查变得低成本的关键。ETag 是一个短标识符(通常是哈希值),由响应的实际内容生成。如果内容哪怕只改变了一个字节,ETag 也会随之改变,使其成为内容的“指纹”。
The revalidation flow looks like this: The server’s first response includes something like ETag: "abc123". The browser remembers that value, and once the cached copy has expired, it sends a follow-up request carrying If-None-Match: "abc123". The server recomputes the content’s current ETag. If it hasn’t changed, it replies with 304 Not Modified — a lightweight response with no body. The browser sees the 304 and keeps using its existing cached copy.
重新验证流程如下:服务器的首次响应包含类似 ETag: "abc123" 的信息。浏览器记住该值,一旦缓存副本过期,它会发送一个携带 If-None-Match: "abc123" 的后续请求。服务器重新计算内容的当前 ETag。如果内容未变,服务器会回复 304 Not Modified——这是一个不包含响应体的轻量级响应。浏览器看到 304 后,会继续使用现有的缓存副本。
The key detail is that a 304 Not Modified response never re-transmits the actual image or file — the server still has to do the work of recomputing whether the content changed, but the network transfer itself is skipped. Where max-age is a blunt instrument (“skip checking entirely for this long”), ETag is a finer one (“even after the window closes, skip re-downloading if nothing actually changed”).
关键细节在于,304 Not Modified 响应永远不会重新传输实际的图片或文件——服务器虽然仍需执行重新计算以判断内容是否改变,但网络传输本身被跳过了。如果说 max-age 是一种粗暴的工具(“在这么长时间内完全跳过检查”),那么 ETag 就是一种更精细的工具(“即使窗口期结束,如果内容没变,也跳过重新下载”)。
Why ogp-generator.php Doesn’t Use ETag
为什么 ogp-generator.php 不使用 ETag
Given the above, it might seem like ogp-generator.php is missing something by not implementing ETag. Looking at the actual code shows the opposite: the design makes revalidation unnecessary in the first place.
鉴于上述情况,看起来 ogp-generator.php 因为没有实现 ETag 而有所欠缺。但查看实际代码会发现恰恰相反:这种设计使得重新验证变得完全没必要。
$modkey = get_post_modified_time('YmdHis', true, $post); // GMT-based
$cache_file = $cache_dir . "/post-{$post_id}-{$lang}-{$modkey}.png";
The generated filename bakes in the post ID, the language, and the post’s last-modified timestamp. When a post is edited and post_modified changes, the resulting PNG’s filename — and therefore its URL — changes along with it. This is a technique commonly called cache busting: instead of asking “has this URL’s content changed?”, the system makes sure a given URL’s content can never change in the first place, because any real change produces a different URL.
生成的文件名中嵌入了文章 ID、语言和文章的最后修改时间戳。当文章被编辑且 post_modified 发生变化时,生成的 PNG 文件名(即 URL)也会随之改变。这是一种通常被称为**缓存清除(cache busting)**的技术:系统不再询问“这个 URL 的内容变了吗?”,而是确保给定的 URL 内容永远不会改变,因为任何实际的更改都会产生一个新的 URL。
Under that design, there’s nothing for ETag to check — a given URL is guaranteed, by its own structure, to point at content that was frozen the moment it was generated. That guarantee is exactly what makes it safe to attach immutable. If the generator instead reused the same filename after a post’s title changed, immutable would be lying to the browser, and editors would see stale OGP images persist for up to 30 days after every edit.
在这种设计下,ETag 根本无事可做——通过其自身的结构,给定的 URL 保证指向的是生成那一刻就被“冻结”的内容。正是这种保证使得附加 immutable 变得安全。如果生成器在文章标题更改后重复使用相同的文件名,那么 immutable 就是在对浏览器撒谎,编辑们在每次编辑后,可能会看到过期的 OGP 图片持续存在长达 30 天。
Both approaches solve the same underlying problem — keeping a cache from serving stale content — but from opposite directions. ETag accepts that the URL stays fixed and pays a small revalidation cost on every expiration cycle to confirm freshness. Baking a version into the URL itself avoids that round trip entirely, at the cost of only working cleanly when there’s a reliable signal (here, post_modified) to version against. Which approach fits depends on how often, and at what granularity, the underlying content actually changes.
这两种方法都解决了同一个根本问题——防止缓存提供过期内容——但方向相反。ETag 接受 URL 保持不变的事实,并在每个过期周期支付少量的重新验证成本来确认新鲜度。将版本号直接嵌入 URL 则完全避免了那次往返请求,代价是只有在有可靠信号(此处为 post_modified)作为版本依据时才能完美运作。哪种方法更合适,取决于底层内容变化的频率和粒度。
Takeaway
总结
Cache-Control and ETag both exist to control caching, but they control different parts of it. Cache-Control’s max-age controls how long a browser can skip contacting the server entirely. ETag controls how cheaply the server and browser can confirm, once that window closes, whether the content has genuinely changed.
Cache-Control 和 ETag 的存在都是为了控制缓存,但它们控制的是缓存的不同部分。Cache-Control 的 max-age 控制浏览器可以完全不联系服务器的时长。ETag 则控制在窗口期结束后,服务器和浏览器能以多低的成本确认内容是否真的发生了变化。