想象你去图书馆借书。管理员告诉你:“这本书有副本,你直接拿走就行,不用登记。”这就是强缓存。但有时管理员会说:“我帮你查一下原版有没有修订,如果没有,你手上的副本还能接着用。”这就是协商缓存。而每次都必须去总馆重新调阅、复印一份新书的,则是完全没有使用缓存的网络请求。 HTTP缓存机制,就是浏览器与服务器之间约定的一套“借书规则”。它决定了哪些资源可以放心地在本地复用(强缓存),哪些资源需要先问服务器“我的副本还新吗”(协商缓存),以及你手里的副本是否已经“借阅过期”。这套机制是Web性能优化的基石——理解它,你就能解释为什么刷新页面时有些资源瞬间加载、有些却要等网络往返。

岗位关联

对AI从业者而言,理解HTTP缓存是排查线上模型服务延迟、优化推理接口响应速度的必备技能——你部署的模型服务,同样要面对“缓存该不该用、怎么用才不出错”的问题。

目录
  1. 可视化演示
  2. 缓存初识
  3. 强缓存规则
  4. 协商缓存规则
  5. 优先级判定
  6. no-cache辨析
  7. 指纹化策略
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

\u5c55\u793a32\u4f4d\u5355\u7cbe\u5ea6\u6d6e\u70b9\u6570\u7684\u5b58\u50a8\u683c\u5f0f\uff1a1\u4f4d\u7b26\u53f7\u4f4d\u30018\u4f4d\u6307\u6570\u4f4d\u300123\u4f4d\u5c3e\u6570\u4f4d\uff0c\u8c03\u6574\u6570\u503c\u53ef\u5b9e\u65f6\u67e5\u770b\u4e8c\u8fdb\u5236\u5206\u89e3\u8fc7\u7a0b\u3002

本节配有交互动效(进入视口后自动加载,约 3KB)。

IEEE 754\u6d6e\u70b9\u6570 S\u7b26\u53f7\u4f4d E\u6307\u6570\u4f4d M\u5c3e\u6570\u4f4d (-1)^S\u00d72^(E-127)\u00d71.M \u8c03\u6574\u6ed1\u5757\u67e5\u770b\u5206\u89e3

缓存初识

继续我们的图书馆。你发现管理员有一套“常借图书登记册”:你第一次借《深度学习》时,管理员从总馆调来一本,并告诉你:“这本书我复印了一份放你桌上,下次直接看复印件。”此后再借,如果复印件还能用,你连管理员都不用找——这是强缓存找副本的直觉。

但在计算机网络里,“复印件”就是浏览器本地磁盘上的缓存文件。HTTP协议设计者们意识到:Web页面里90%的资源(图片、CSS、JS)在短时间内根本不会变化,每次访问都从服务器重新下载,是巨大的浪费。于是有了缓存机制——把服务器返回的资源连同“保鲜期”一起存下来。

HTTP缓存分两大类:强缓存(浏览器自认为副本新鲜,完全不发请求)和协商缓存(浏览器不确定副本是否新鲜,发一个轻量请求让服务器做裁决)。两者的共同点是都用上了本地副本;区别在于,强缓存省掉了整个网络往返,协商缓存则多了一次“确认”。

那么,是不是缓存越多越好?也不是。缓存就像复印本——如果原著已经修订,你还捧着旧复印件,就会看到过时信息。这就是缓存与“新鲜度”的永恒矛盾:缓存要快,就必须大胆复用;但要准确,就必须小心验证。强缓存与协商缓存,正是解决这一矛盾的两把尺子。

缓存命中率=由缓存直接响应的请求数总请求数×100%\text{缓存命中率} = \frac{\text{由缓存直接响应的请求数}}{\text{总请求数}} \times 100\%
命中率越高,说明越多的请求没有真正打到服务器,页面加载自然越快。这是衡量缓存配置好坏的第一个指标。
  1. 响应请求数所有未真正访问网络的请求,例如强缓存直接命中、协商缓存返回304(也计为命中但需一次往返)。
    由缓存直接响应的请求数\text{由缓存直接响应的请求数}
  2. 总请求数页面发起的全部HTTP请求,包含命中与未命中的。
    总请求数\text{总请求数}
  3. 比值与百分比两者相除后乘以100,得到0到100之间的百分比。
    ×100%\times 100\%

强缓存规则

现在管理员给了你一张“绿色通行证”:某些书,你在借阅期内可以直接从桌上拿走副本,连登记都省了。这张通行证上写着有效期——这就是强缓存。HTTP协议里,服务器通过响应头告诉浏览器:“这个资源,在过期之前你都别来问我,直接用本地副本。”

强缓存的核心指令是Cache-Control,它是HTTP/1.1时代制定的标准,也是最灵活的缓存控制头。它的取值组合非常丰富:max-age=3600表示副本在3600秒内新鲜;public表示任何节点(包括代理服务器)都能缓存;private表示仅供单个用户缓存。与之相对的是HTTP/1.0时代的Expires,它用一个绝对时间戳(如Wed, 21 Oct 2025 07:28:00 GMT)指定过期时刻。

如果响应头同时出现了Cache-Control和Expires,以Cache-Control为准——它是更现代的规则,能处理时区、时钟偏移等问题。比如Expires是绝对时间,一旦用户电脑时间不准,强缓存就立刻失效;而max-age是相对时间,从响应时刻开始计时,天然免疫时钟错误。

强缓存的命中不需要任何网络请求,浏览器直接从本地读取文件。所以你在DevTools里常见的"200 (from disk cache)",就是强缓存命中的标志。它的速度极快,近乎瞬时。但代价是:在保鲜期内,服务器即使更新了资源,你也看不见。

缓存剩余寿命=max-age−(当前时间−响应时间)\text{缓存剩余寿命} = \text{max-age} - (\text{当前时间} - \text{响应时间})
剩余寿命大于0,强缓存命中;小于等于0,则进入协商缓存或重新请求。
  1. 设置保鲜期服务器在响应头写入Cache-Control: max-age=3600,等价于告诉浏览器“接下来1小时内,这个副本都是新鲜的”。
    max-age=3600s\text{max-age} = 3600\text{s}
  2. 记录响应时间浏览器收到响应时,记下当前时间作为起点。
    响应时间=t0\text{响应时间} = t_0
  3. 计算剩余寿命用保鲜期减去已经过去的时间,得到的结果就是副本还能直接用的时长。
    剩余寿命=max-age−(tnow−t0)\text{剩余寿命} = \text{max-age} - (t_{\text{now}} - t_0)

协商缓存规则

借阅期的概念再推进一步:假设管理员说,“这些书你桌上的副本可以一直用,但我要求你每次来还书时,顺口问我一句'这书改过没'?如果我说没改,你就继续用旧副本。”这就是协商缓存——本地副本可能过期,但浏览器愿意先花一次轻量请求,换取“可能不用下载完整资源”的机会。

协商缓存有两个判据:Last-Modified(最后修改时间)和ETag(实体标签)。Last-Modified是服务器返回资源时附带的“最后修改时间”,浏览器下次请求时通过If-Modified-Since把这个时间带回给服务器,服务器对比当前资源的修改时间,若未变化则返回304状态码。

ETag则更精确——它是服务器为资源内容生成的哈希指纹(如"33a64df5"),只要内容变了,ETag就变。浏览器通过If-None-Match把ETag带回,服务器比对当前资源的ETag:

那Last-Modified和ETag谁更好?ETag更可靠——因为文件可能改了又改回原样,修改时间变了但内容一致;或者修改时间精度不够(如1秒内多次修改)。但ETag需要额外计算哈希。实践中通常同时使用:请求时两个验证头都带上,服务器先比对If-None-Match,若不一致再比对If-Modified-Since。只要任意一个验证通过,就返回304。

协商缓存的价值在于“用一次轻往返换掉一次重下载”。对于大体积的静态资源(几MB的JS文件),304省下的流量和带宽相当可观。它不像强缓存那样零请求,但比完全重新下载高效得多。

若ETag请求=ETag当前⇒返回 304 Not Modified\text{若} \quad \text{ETag}_{\text{请求}} = \text{ETag}_{\text{当前}} \quad \Rightarrow \quad \text{返回 304 Not Modified}
304响应不带响应体,只带几个字节的头信息,相比重新下载整个资源,节省的流量可达几十到几百倍。
  1. 生成指纹服务器在首次响应时,用资源内容计算出一个哈希字符串。
    ETag=hash(资源内容)\text{ETag} = \text{hash}(\text{资源内容})
  2. 回传验证浏览器再次请求时,把ETag放入If-None-Match请求头。
    If-None-Match: "33a64df5"\text{If-None-Match: "33a64df5"}
  3. 服务器比对服务器计算当前资源的ETag,和请求头里的值比较。
    ETag请求=?ETag当前\text{ETag}_{\text{请求}} \stackrel{?}{=} \text{ETag}_{\text{当前}}
  4. 结果分岔相等则304(未修改),不相等则200+新资源。
    相等: 304不等: 200+新资源\text{相等: }304 \quad\text{不等: }200 + \text{新资源}

优先级判定

现在你手上有两本规则手册:一本写着“绿色通行证优先,过期了再去问管理员”,另一本写着“就算没过期,管理员也可能要求你验证”。当资源同时带有强缓存和协商缓存的条件时,浏览器遵循一个严格的顺序——先强缓存,后协商缓存。

这个优先级顺序是浏览器内置的:

特别注意:Cache-Control内部也有优先级。比如no-cache这个指令,它的意思是“可以用缓存,但每次都得先验证”,所以它实际上会强制跳过强缓存、直接进入协商缓存阶段。而max-age=0也有类似效果——剩余寿命恰好为0,强缓存失效,转入协商。

代理缓存的引入让优先级更加复杂:浏览器与源站之间可能还有CDN节点。浏览器先查自己的强缓存,没命中就请求CDN;CDN再查它的强缓存,没命中就回源站验证。每一层都是“先强后协”的递归结构。理解了这点,你就理解了整个Web缓存的骨架。

请求流程: 检查强缓存⏟命中则直接返回→协商缓存验证⏟304则用本地→重新下载⏟200+新资源\text{请求流程: } \underbrace{\text{检查强缓存}}_{\text{命中则直接返回}} \rightarrow \underbrace{\text{协商缓存验证}}_{\text{304则用本地}} \rightarrow \underbrace{\text{重新下载}}_{200 + \text{新资源}}
强缓存是“第一道闸门”,协商缓存是“第二道闸门”,只有两道都放行不了,才真正去服务器下载完整资源。
  1. 第一道闸门浏览器先检查Cache-Control/Expires,若副本新鲜则直接命中,整个流程到此结束。
    强缓存新鲜?⇒直接使用本地副本\text{强缓存新鲜?} \Rightarrow \text{直接使用本地副本}
  2. 第二道闸门强缓存过期后,浏览器携带If-None-Match/If-Modified-Since发送请求。
    协商缓存验证?⇒304则复用本地\text{协商缓存验证?} \Rightarrow \text{304则复用本地}
  3. 最后手段服务器返回200,浏览器下载新内容并更新缓存。
    200 OK, 下载新资源, 更新本地副本\text{200 OK, 下载新资源, 更新本地副本}

no-cache辨析

“no-cache“这个名字极具迷惑性——字面意思是”不要缓存“,但它真正的含义是”可以用缓存,但每次用之前必须验证“。这就像图书馆的”禁带出馆”书籍:你可以坐着看,但每次都要跟管理员登记一次。

所以Cache-Control: no-cache的效果是:强制跳过强缓存,每次请求都走协商缓存流程。它不禁止存储,只是禁止“未经验证的直接复用”。而Cache-Control: no-store才是真正的“禁止缓存”——浏览器和中间代理都不得保存任何副本。

这两个指令的误用代价很高。如果给静态资源加了no-store,每次刷新页面都要重新下载全部资源,配置CDN缓存时会被回源请求淹没;如果给包含敏感信息的API响应加了no-cache(而非no-store),虽然每次会验证,但缓存副本仍留在磁盘上,有安全风险。正确的姿势是:静态资源用max-age+指纹,动态HTML用no-cache,敏感数据用no-store。

no-cache: 存储=允许,直接复用=禁止,需验证\text{no-cache: } \text{存储} = \text{允许}, \quad \text{直接复用} = \text{禁止}, \quad \text{需验证}
no-store: 存储=禁止\text{no-store: } \text{存储} = \text{禁止}
一句话区分:no-cache是“每次都要问”,no-store是“压根不让借”。
  1. no-cache语义允许浏览器保存副本,但每次使用前必须发请求给服务器验证有效性。
    no-cache⇒每次使用前验证\text{no-cache} \Rightarrow \text{每次使用前验证}
  2. no-store语义禁止任何缓存组件保存副本,每次请求都必须从源站获取完整资源。
    no-store⇒不允许保存副本\text{no-store} \Rightarrow \text{不允许保存副本}
  3. 混用场景两者可以同时出现,但其实no-store已经包含了no-cache的语义。

指纹化策略

图书馆有个聪明的管理员:他给每本书的修订版起了个新名字《深度学习·第3版》。当第4版出来时,读者借的是《深度学习·第4版》,永远不会拿错版本。这就是指纹化——给静态资源的文件名打上内容哈希。

现代前端构建工具(Webpack、Vite)会自动把app.js变成app.a3f9d2.js,其中a3f9d2是文件内容的哈希值。只要文件内容变了,哈希就变,文件名也随之改变。这个新文件名意味着它是一个全新的资源,可以放心地设置超长max-age(比如一年)。

指纹化策略与CDN配合时,威力更大。假设你的静态资源部署在CDN上,CDN节点会缓存app.a3f9d2.js一年。当app.b4c0e1.js(新版本)上线时,用户的浏览器发现需要请求一个全新的URL,于是绕过旧缓存,直接从CDN拉取新文件——这一过程中,CDN的旧缓存依然有效,无需手动刷新。

但这里有个陷阱:index.html本身不能设长缓存。因为HTML是“入口文件”,它引用的JS/CSS文件名会随版本变化,如果HTML被强缓存一年,用户就永远看到旧页面。标准做法是:HTML用no-cache(每次验证),静态资源用指纹+长缓存。这样HTML始终最新,而静态资源只下载一次。

新文件名=原文件名+hash(文件内容)\text{新文件名} = \text{原文件名} + \text{hash(文件内容)}
Cache-Control: max-age=31536000, immutable\text{Cache-Control: max-age=31536000, immutable}
immutable表示“文件永不变”,浏览器连协商验证都省了,直到文件名变化。
  1. 计算内容哈希构建工具读取文件内容,计算哈希值(如MD5、SHA-1的短片段)。
    hash=MD5(文件内容)\text{hash} = \text{MD5}(\text{文件内容})
  2. 拼接新文件名将哈希嵌入文件名,内容一变,文件名就变。
    app.a3f9d2.js\text{app.a3f9d2.js}
  3. 设置超长缓存因为文件名与内容一一对应,文件名不变的资源内容必然不变,可以放心使用极长缓存时间。
    max-age=31536000\text{max-age}=31536000

AI 应用场景

模型服务API的缓存编排

阶段:模型推理服务部署与优化

解决的问题:AI模型的推理结果通常需要几百毫秒到几秒,如果同一个请求(如文本分类、图像识别)反复打满GPU,成本极高且延迟不可控。HTTP缓存可以在不降低服务质量的前提下,把重复请求的响应时间从秒级降到毫秒级——强缓存命中时甚至不经过网络,协商缓存则用ETag验证请求参数是否变化。

具体案例:某内容审核API,对同一张图片的重复审核请求,服务端返回Cache-Control: max-age=300 + ETag。CDN层存储审核结果,99%的重复请求在CDN节点直接命中,源站GPU负载下降70%。当审核策略调整(如新增违禁词库)时,通过更新ETag强制协商缓存失效,实现“秒级生效、全网同步”。

大模型Web应用的静态资源加速

阶段:AI应用前端交付

解决的问题:LLM Web应用(如ChatGPT类产品)的JS包动辄数MB,每次刷新都完整下载会严重拖慢首屏。利用指纹化策略让JS/CSS长缓存,用户二次访问时几乎秒开;用no-cache保护index.html,保证新功能上线后用户能第一时间拉到最新入口。

具体案例:某AI绘画Web应用,原来每次发布新版本都要引导用户强制刷新(Cmd+Shift+R),否则会白屏报错。引入指纹化后,构建产物自动携带哈希,HTML走协商缓存验证版本,JS/CSS走一年强缓存——发布新版本后,用户刷新页面即自动加载新版,无需任何手动操作。

特征数据接口的缓存分层

阶段:特征工程与在线推理

解决的问题:推荐系统的特征服务每天被调用数十亿次,特征数据(如用户画像)在几分钟内通常不会有变化。如果能缓存这些特征,就能大幅降低特征存储层的QPS压力。用Cache-Control: max-age=60, private对用户级特征做强缓存,用ETag对群体特征做协商验证,在延迟和新鲜度之间找到平衡。

具体案例:某电商推荐系统,用户兴趣标签接口加缓存后,P99延迟从85ms降至12ms,特征存储集群的QPS压力减少60%。同时用ETag的版本号机制,在推荐策略迭代时对特征缓存做批量失效,保证模型能及时感知特征分布变化,避免“旧特征喂新模型”的线上线下不一致问题。

Python 演示

python87 行
# -*- coding: utf-8 -*-"""HTTP缓存机制模拟器:演示强缓存与协商缓存的完整流程"""import hashlibimport time  class CacheEntry:    def __init__(self, data, max_age, etag=None, last_modified=None):        self.data = data        self.max_age = max_age        self.etag = etag or hashlib.md5(data.encode()).hexdigest()[:8]        self.last_modified = last_modified or time.time()        self.stored_at = time.time()     def is_fresh(self):        return (time.time() - self.stored_at) < self.max_age  class Server:    """模拟源站:资源会随时间变化"""     def __init__(self):        self.content = "v1: hello world"        self.updated_at = time.time()     def update(self, new_content):        self.content = new_content        self.updated_at = time.time()     def get_response(self, if_none_match=None, if_modified_since=None):        etag = hashlib.md5(self.content.encode()).hexdigest()[:8]        last_modified = time.strftime(            "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(self.updated_at)        )        if if_none_match == etag:            return 304, None, etag, last_modified        if if_modified_since and self.updated_at <= if_modified_since:            return 304, None, etag, last_modified        return 200, self.content, etag, last_modified  class Browser:    """模拟浏览器缓存行为"""     def __init__(self, server):        self.server = server        self.cache = {}        self.request_count = 0     def fetch(self, url):        self.request_count += 1        entry = self.cache.get(url)         if entry and entry.is_fresh():            return "strong-cache-hit", entry.data         if entry:            status, data, etag, lm = self.server.get_response(                if_none_match=entry.etag,                if_modified_since=entry.last_modified,            )            if status == 304:                self.cache[url] = CacheEntry(                    entry.data, entry.max_age, etag, entry.last_modified                )                return "negotiated-304", entry.data         status, data, etag, lm = self.server.get_response()        self.cache[url] = CacheEntry(data, max_age=10, etag=etag)        return "fresh-200", data  def main():    srv = Server()    browser = Browser(srv)     for i in range(5):        res, body = browser.fetch("/api/news")        print(f"请求{i+1}: {res} | 内容: {body}")        if i == 2:            print("[服务器更新内容]")            srv.update("v2: hello world, refreshed")        time.sleep(0.5)  if __name__ == "__main__":    main()
目的:用Python模拟浏览器缓存的三态变化——强缓存命中、协商缓存304、完整下载200,帮助你直观理解HTTP缓存的核心判断逻辑。
  1. class CacheEntry缓存条目,记录内容、保鲜期(max_age)、ETag指纹和存储时间,对应浏览器缓存的元数据。
  2. def is_fresh(self)判断剩余寿命是否大于0,这是强缓存命中的核心条件。
  3. class Server模拟源站,维护资源内容和最后修改时间,update方法模拟服务器更新资源。
  4. def get_response服务器核心逻辑:先比ETag,再比Last-Modified,条件满足就返回304。
  5. if if_none_match == etag:服务器端ETag精准比对,内容只要变过,ETag必然不同。
  6. class Browser模拟浏览器,维护本地缓存字典和请求计数器。
  7. if entry and entry.is_fresh():第一道闸门:强缓存命中,直接返回本地副本,零网络请求。
  8. if status == 304:第二道闸门:协商缓存,服务器确认副本仍有效,用旧的但不重新下载。
  9. self.cache[url] = CacheEntry(...)200响应后更新本地缓存,写入新的ETag和保鲜期。

常见误区

误区我一开始以为`no-cache`就是完全不缓存,每次都要重新下载。给所有静态资源都加了`no-cache`,结果页面刷新特别慢,因为每个CSS、JS都要重新拉一遍。

正解no-cache的真实含义是“允许缓存,但每次使用前必须向服务器验证”。它禁止的是“不经验证的直接复用”,而不是“存储”。就像图书馆的“馆内阅览”——书可以放在你桌上,但每次拿起来看都要登记一次。真正禁止存储的是no-store。把静态资源加no-cache,等于放弃了强缓存的全部收益,但也没获得no-store的安全性,是最亏的配置。

为何常见:因为指令的英文字面意思太有欺骗性,“cache“这个词让人直接联想到”缓存“,所以”no-cache“自然被理解成”无缓存”。HTTP规范文档里对它的定义又比较绕,非专业读者很容易望文生义。

误区我担心给静态资源设置一年`max-age`后,用户就永远看到旧页面。等我自己改了代码重新部署,发现很多用户还在用老版本,就再也不敢用强缓存了。

正解这个问题的解法不是放弃缓存,而是改变缓存的“键”。指纹化策略让文件名随内容变化——新版本发布后,HTML引用的JS/CSS文件名不同,浏览器会当作全新资源请求,完全绕开旧缓存。就像图书馆给修订版起新书名,读者借《深度学习·第4版》自然拿不到第3版的内容。关键在于:可以对静态资源设长缓存,但绝不能对HTML设长缓存,因为HTML是入口,必须保持最新。

为何常见:大多数开发者的第一次缓存优化都是“设个max-age”这种单一维度操作,没意识到缓存策略需要“入口文件动态化、子资源静态化”的组合拳。加上浏览器刷新(F5)和强制刷新(Ctrl+F5)行为不同,更加深了“缓存导致更新失败”的错觉。

误区我图省事,只用了`Last-Modified`。结果有一次资源在1秒内被修改了两次,第二次修改后内容其实和第一次一样,但浏览器的请求还是拿到了200而不是304,白白下载了一遍。

正解Last-Modified的精度是秒级,而且只能反映“修改时间”,无法感知“内容是否真的变了”。ETag是内容哈希,只要字节不同它就不同。正确的做法是同时使用:服务器先比对If-None-Match,不一致再比对If-Modified-Since。对于CDN回源、负载均衡器转发的场景,代理服务器可能改写资源,Last-Modified会失真,ETag依然是最终裁决者。

为何常见:Last-Modified是HTTP/1.0时代就有的老字段,资料多、用起来简单,而ETag是HTTP/1.1才引入的。很多人接触到的是老教程,自然只学会了前者,不知道两者其实可以取长补短。

误区我负责的是API后端,觉得缓存是前端和CDN的事,后端接口不需要考虑HTTP缓存。结果同一份IDEA数据每天被不同用户反复请求,数据库压力很大。

正解API接口同样能从缓存获益——Cache-Control: private允许浏览器缓存但禁止共享缓存,适合用户维度的个性化数据;public配ETag适合内容不怎么变化的公共数据。AI推理服务是更典型的例子:相同的输入图片在审核、OCR场景中经常重复出现,用ETag验证加CDN缓存,可以把重复请求的GPU负载几乎降为零。缓存不是静态资源专属,而是要区分数据的“隐私性”和“易变性”来选择策略。

为何常见:教程和博客大多用CSS、JS举例讲缓存,给人造成“缓存=静态资源”的思维定式。实际上HTTP缓存是一种通用协议机制,与资源类型无关,关键是响应头怎么配。

误区我在服务器上给接口加了`Cache-Control: max-age=60`,但测试时发现每次请求都还是打到服务器,根本没走缓存。我以为是浏览器不行,换了好几个浏览器都一样。

正解max-age生效的前提是响应状态码可缓存(如200),且没有no-cache、no-store等更强制令干扰。更常见的原因是:代理或CDN层可能把Cache-Control头剥离或改写,浏览器收到的响应头里根本没有max-age。另外,POST请求默认不可缓存,只有GET才能利用缓存。排查时先看DevTools里"Response Headers"是否真的有缓存指令,再确认请求方法是GET、状态码是200。这就像管理员给你发了张通行证,但快递寄丢了——你手里根本没有证。

为何常见:很多人只关注“应用层怎么设”,忽略了中间链路(Nginx、CDN、云厂商WAF)可能改写响应头。加上浏览器对不同状态码、不同请求方法的缓存策略有隐藏规则,实际生效情况与直觉差异很大。

面试题

Q1. 强缓存命中时,浏览器会发生什么?

单选题 · easy

  • A.发送请求到服务器,服务器返回304
  • B.不发送请求,直接从本地缓存读取资源← 正确答案
  • C.发送请求到服务器,服务器返回200
  • D.发送请求到CDN节点重新下载

答案:B。强缓存的核心特征就是“零网络请求”——浏览器根据Cache-Control或Expires判断副本还在保鲜期内,直接走"200 (from disk cache)"。其他选项描述的都是协商缓存(304)或缓存过期(200)的场景。就像你桌上的复印本还在有效期内,管理员放行,你直接看即可,不用再去柜台登记。

Q2. Cache-Control: no-cache的正确含义是?

单选题 · easy

  • A.禁止浏览器保存任何缓存副本
  • B.允许缓存,但每次使用前必须向服务器验证← 正确答案
  • C.只允许代理服务器缓存,源站不缓存
  • D.缓存时间设为0秒,之后立即失效

答案:B。no-cache是“可以用,但每次都要验证”,它会强制跳过强缓存阶段直接进入协商缓存。真正禁止存储的是no-store。选项A是no-store的含义,C是public/private的混淆,D误将相对时间指令当成“立即过期”。

Q3. 关于ETag和Last-Modified,下列说法正确的是?

单选题 · easy

  • A.ETag是绝对时间戳,Last-Modified是内容哈希
  • B.ETag是内容哈希,Last-Modified是最后修改时间← 正确答案
  • C.两者都是时间戳
  • D.两者都是内容哈希

答案:B。ETag是服务器为资源内容生成的唯一标识(通常是哈希值),内容一变ETag就变;Last-Modified是资源的最后修改时间。ETag比时间戳更精确,能感知“修改时间没变但内容变了”的情况,所以两者同时使用时,服务器优先比对ETag。

Q4. 浏览器请求一个资源时,正确的缓存判断顺序是?

单选题 · mid

  • A.先协商缓存验证,再检查强缓存是否新鲜,最后重新下载
  • B.先检查强缓存是否新鲜,新鲜则直接使用;过期再协商缓存验证;最后重新下载← 正确答案
  • C.先重新下载,再判断是否缓存
  • D.先发请求给CDN,再检查本地缓存

答案:B。浏览器的缓存流程是“两道闸门”:第一道强缓存判断副本是否新鲜,新鲜就终止流程;不新鲜才进入第二道协商缓存,携带ETag/Last-Modified验证;验证失败才重新下载。就像你先看桌上复印本的日期,过期了才去问管理员,而不是一上来就去找管理员。

Q5. 指纹化策略(文件名加哈希)的核心原理是?

单选题 · mid

  • A.让服务器可以加密资源内容
  • B.让文件名随内容变化,从而安全地使用长缓存← 正确答案
  • C.让浏览器每次都能找到相同文件名的资源
  • D.让CDN可以压缩资源体积

答案:B。指纹化把内容哈希拼进文件名,app.a3f9.js与内容一一对应——内容不变文件名就不变,内容变了文件名就变。这样旧文件可以放心设置一年max-age,新文件因为是全新URL自然绕开缓存。这就是“内容寻址”的威力,与加密、压缩无关。

Q6. 以下哪个响应头组合最适合“每个用户的个性化数据,且不希望被CDN共享缓存”?

单选题 · mid

  • A.Cache-Control: public, max-age=3600
  • B.Cache-Control: private, max-age=3600← 正确答案
  • C.Cache-Control: no-store, max-age=0
  • D.Cache-Control: proxy-revalidate, max-age=60

答案:B。private明确禁止共享缓存(CDN、代理),只允许浏览器私有缓存,完美匹配“个性化数据”的场景;max-age=3600允许浏览器在一小时内复用。public会让CDN也缓存,可能把用户A的数据发给用户B。no-store连浏览器也不给缓存,牺牲了缓存收益。

Q7. 关于304 Not Modified响应,以下说法正确的是?

多选题 · mid

  • A.304响应包含完整的响应体
  • B.304响应体为空,只包含少量头信息← 正确答案
  • C.304表示服务器确认本地副本仍然有效← 正确答案
  • D.304表示资源已永久删除

答案:BC。304 Not Modified的响应体为空,它的全部意义就是“用头信息告诉浏览器:你这副本还能用”——这就是协商缓存的精髓:用几十个字节的头替代几MB的响应体,节省大量带宽。选项D的“永久删除”是410状态码,A与B的事实相反。

Q8. 哪些情况下会绕过强缓存,直接向服务器发送请求?

多选题 · mid

  • A.用户按Cmd/Ctrl+F5强制刷新← 正确答案
  • B.缓存副本已超过max-age← 正确答案
  • C.响应头包含Cache-Control: no-cache← 正确答案
  • D.资源URL以.css结尾

答案:ABC。强制刷新会在请求头加Cache-Control: no-cache绕过强缓存;max-age过期自然进入协商或重新请求;响应头带no-cache则每次都要验证。而资源后缀名与缓存无直接关系——只要响应头允许,.css文件也能命中强缓存。D选项是常见误解。

Q9. 服务器同时收到If-None-Match: "abc"和If-Modified-Since: Tue, 15 Aug 2023 10:00:00 GMT,当前资源ETag为"def",最后修改时间为2023年8月16日。服务器应返回?

单选题 · hard

  • A.304,因为Last-Modified之后内容未变
  • B.200 + 新资源,因为ETag不一致← 正确答案
  • C.200 + 新资源,因为Last-Modified变了
  • D.404,因为资源已删除

答案:B。服务器对协商缓存请求的判定顺序是:先比对ETag,ETag不一致(abc vs def)就直接返回200+新资源,不再看Last-Modified。这不是说Last-Modified没用了——在ETag一致或没有ETag时才轮到它。这里ETag已经判定内容不同,说明资源确实更新了,必须返回新内容。B选项里的“Last-Modified变了”与最终返回200的判断无关,因为ETag已经做出了裁决。

Q10. 一个大型Web应用希望“静态资源长缓存、入口HTML永远最新、敏感接口不落盘”。合理的配置是?

多选题 · extreme

  • A.app.a3f9.js设置Cache-Control: max-age=31536000, immutable← 正确答案
  • B.index.html设置Cache-Control: no-cache← 正确答案
  • C./api/user/profile设置Cache-Control: no-store← 正确答案
  • D.所有资源统一设置Cache-Control: public, max-age=86400

答案:ABC。指纹化后的静态资源内容不可变,用一年长缓存+immutable是最优解;HTML作为入口必须每次验证(no-cache),否则发布新版本后用户拿不到新入口;敏感API(含用户身份数据)用no-store防止落盘泄露。D选项把所有资源一刀切设置为public,会让CDN缓存用户敏感数据,是严重的安全隐患。这套组合正是现代前端性能优化的标准范式。