在页面上用 <script src="https://cdn.example.com/app.xxxx.js"></script> 引用一段外部 JS,再顺手给静态服务器加一行 Cache-Control: max-age=31536000,这几乎是所有前端和运维都做过的常规操作。可一旦你真去追这个响应头,会发现自己踩进了一个由 HTTP 缓存语义、资源版本管理、中间代理策略和浏览器行为共同织成的网。标题里“庖丁解牛”四个字用得很准,因为只背这一行配置没什么用,真正值钱的是把整个链路拆开看明白:为什么是 31536000,谁在消费这个字段,和页面更新冲突时该怎么办,以及最后怎么才能安全地享受长缓存带来的性能红利。
这篇文章适合正在被“用户浏览器打开还是旧脚本”折磨的前端,也适合刚接手静态资源服务、想搞清楚缓存头怎么配才不背锅的运维和全栈。读完后你至少能回答三个问题:外部 JS 的 Cache-Control 到底由谁设置、max-age=31536000 为什么是“一年”、在资源可能需要更新的前提下这个值怎么用得不出事。
1. 先从“一整年缓存”说起:外部JS缓存参数的经济账
1.1 外部 JS 的 Cache-Control 到底是谁设置的
很多初学同学有个误解:看到 <script src="xxx.js">,以为在页面里写 JS 代码就能控制它加载文件的缓存。这是把“页面”和“资源”两个角色弄混了。外部 JS 对页面来说是一个独立的 HTTP 请求,它的响应是否被缓存、缓存多久,只取决于那一次 HTTP 响应里返回的 Cache-Control 响应头。发出请求的是浏览器,返回资源的是服务器、CDN 或对象存储,和 JS 文件内部写了什么内容没有直接关系。
举个极端例子:假如我在自己的网站服务一个 tracker.js,并且服务器配置了:
code复制Cache-Control: public, max-age=31536000
那么即使 JS 文件里的代码每天自动变化,只要文件 URL 不变,浏览器在一年内都不会再向服务器发起这个文件的请求。外部 JS 本身没有任何能力去“修改”这次响应头——真正起作用的,是服务器在收到请求后返回的 HTTP 头部。你想让外部 JS 获得这个缓存策略,只能去改资源所在服务器的响应配置,而不是在 JS 代码里写 localStorage 之类的东西。
明白这一点后,后面所有操作都围绕同一件事:如何让提供 JS 资源的服务端在处理请求时,稳定地返回一行我们希望见到的 Cache-Control。
1.2 31536000 这个数字是怎么来的,为什么写错的人很多
Cache-Control 里的 max-age 单位是秒,不是毫秒,更不是天。31536000 是由 60 秒 × 60 分钟 × 24 小时 × 365 天计算得到,也就是一整年。很多团队第一次配长缓存时会顺手写 max-age=315360000,多打一个 0,瞬间变成十年。等到资源需要下线时,想强制让所有客户端丢弃这个缓存几乎只能靠改 URL,相当被动。
这里穿插一个常见误区:max-age 并不是一个绝对过期时间,而是一个相对时间。它表示“从浏览器收到这个响应开始,后续 N 秒内,可以直接使用本地缓存,不需要向服务器验证”。也就是说,如果用户 2025 年 1 月 1 日拿到响应,那么这个资源在本地有效到 2026 年 1 月 1 日;但如果是 2025 年 6 月才首次访问,它就从 6 月开始算一年。
有人会问:为什么不能像 Expires 那样直接写一个 GMT 时间?因为在现代 HTTP 语义里,Cache-Control 的优先级高于 Expires,而且 max-age 是相对时间,不用客户端和服务器各自时钟同步,省去很多时间不准导致的缓存失效问题。这也是为什么很多 CDN 和框架默认都生成 Cache-Control: max-age=... 而不是只发 Expires。
1.3 长缓存对“外部 JS”场景到底意味着什么
外部 JS 是前端性能优化里优先级极高的静态资源。一个中大型站点的主脚本往往有几十到几百 KB,如果是首屏依赖的逻辑,每次刷新都回源会浪费大量带宽,也会让首屏变慢。设置一年长缓存,理论上可以让绝大多数重复访问用户直接命中本地缓存,彻底消灭这部分请求的往返时间。
但“最大缓存一年”不能和“缓存一定会持续一年”划等号。真实场景里还要看用户使用的浏览器磁盘缓存是否足够、是否主动强制刷新、CDN 节点有没有覆盖源站响应头、以及资源 URL 是否发生变化。可以说,max-age=31536000 是在给你提供一张“上限额度”,而不是承诺它能满额使用。想真正拿到长缓存的红利,光配一行响应头远远不够,还需要配合版本号策略和 HTML 的缓存控制,后面我会单独拆一节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 判断现状:动手前必须学会查看当前响应头
“庖丁解牛”里最精彩的一句话是“目无全牛”。意思是解牛时看到的已经不是一整头牛,而是骨骼、经络之间的缝隙。排查缓存问题也一样,如果你只看页面能不能打开,永远抓不住问题;你需要像看结构一样看清一次资源请求从发出到返回过程中,到底带了哪些响应头。
2.1 在命令行里对任意外部 JS 发一次请求
无论目标资源在不在你的服务器上,都可以先用 curl 看它的响应头。比如:
bash复制curl -sI https://example.com/target.js
如果你想同时看到请求状态码和完整响应头,也可以用:
bash复制curl -s -o /dev/null -D - https://example.com/target.js
-o /dev/null 是丢到响应体,-D - 是把响应头打印到终端。执行后,重点找几行:
code复制HTTP/2 200
cache-co
