普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月21日首页

CSS 里的"结界":BFC 与层叠上下文的渲染隔离逻辑

作者 yuki_uix
2026年4月21日 13:22

在写 CSS 的过程中,你可能遇到过这样的困惑:明明没动什么,一个浮动元素突然撑开了父容器;或者费尽心思调 z-index,元素就是不按预期叠放。背后大概率涉及两个概念:BFC(Block Formatting Context,块级格式化上下文)层叠上下文(Stacking Context)

这篇文章是我整理这两块知识的笔记。它们看似是两个独立的"规则",但我理解下来,其实都指向同一件事——浏览器在渲染时划定的"隔离结界" 。只是一个管的是盒子的布局,另一个管的是图层的叠放。


BFC 是什么,为什么需要它?

BFC 的官方定义很抽象:它是一个独立的渲染区域,内部的盒子按照特定规则排列,且与外部互不影响。

我更喜欢把它理解成:一个布局上的"隔离容器"

浏览器在做普通流布局时,float、margin 折叠等行为会在相邻元素之间"渗透"。BFC 的存在,就是划一道边界,宣告:边界内的布局由我自己管,外面的事不干涉进来。

BFC 的触发条件

以下属性会触发 BFC(部分常用条件):

/* 场景:浮动元素 */
.parent {
  overflow: hidden; /* 经典触发方式 */
}

/* 或者使用 display: flow-root(语义更明确,现代写法) */
.parent {
  display: flow-root;
}

/* 其他触发方式 */
.container {
  float: left;       /* 浮动元素本身也是 BFC */
  position: absolute;
  position: fixed;
  display: flex;
  display: grid;
  display: inline-block;
  overflow: auto;
  overflow: scroll;
}

BFC 能解决的三类经典问题

① 清除浮动(高度塌陷)

<!-- 问题:子元素全部浮动,父容器高度为 0 -->
<div class="parent">
  <div class="float-child">浮动子元素</div>
</div>
/* 浮动子元素脱离文档流,父容器感知不到它的高度 */
.float-child {
  float: left;
  height: 100px;
}

/* 触发父容器的 BFC,让它"负责"包含浮动子元素 */
.parent {
  overflow: hidden; /* 高度塌陷解决了 */
}

BFC 有一条规则:BFC 在计算高度时,需要包含内部的浮动元素。所以触发 BFC 之后,父容器就能撑开了。

② 阻止 margin 折叠

/* 普通流中,相邻兄弟元素的 margin 会合并(取较大值) */
.box-a { margin-bottom: 20px; }
.box-b { margin-top: 30px; }
/* 实际间距是 30px,而不是 50px */

/* 如果想阻止折叠,可以给其中一个元素套一个 BFC 容器 */
.wrapper {
  overflow: hidden; /* 触发 BFC */
}
/* 现在 .box-b 在 BFC 内,margin 不再与外部折叠,间距变回 50px */

BFC 内的 margin 不会与外部折叠——这是"隔离"的体现。

③ 防止浮动元素覆盖普通文本

/* 普通流元素默认会被浮动元素遮盖(虽然文字会环绕) */
.float-box { float: left; width: 100px; }
.text-box { overflow: hidden; } /* 触发 BFC,变成自适应两栏布局 */

BFC 不会与浮动元素的盒子重叠,这常用来实现不定宽的两栏布局。


层叠上下文是什么?

如果说 BFC 是平面布局的"结界",那层叠上下文就是 Z 轴方向的"结界"

层叠上下文(Stacking Context)定义了一组元素的 z 轴叠放顺序。每个层叠上下文内部有自己的叠放规则,且整体作为一个单元参与父上下文的叠放

层叠上下文内部,元素从下到上的叠放顺序大致如下:

(底部)
  1. 层叠上下文的背景和边框
  2. z-index 为负值的子层叠上下文
  3. 普通流中的块级元素(非浮动、非定位)
  4. 浮动元素
  5. 普通流中的行内元素
  6. z-index 为 0 或 auto 的定位元素
  7. z-index 为正值的子层叠上下文
(顶部)

z-index 的比较,只在同一个层叠上下文内才有意义。这是很多人调 z-index 调不对的根本原因。


为什么这些属性会触发层叠上下文?

这是我觉得最值得深挖的部分。MDN 列出了十几种触发条件,背后的逻辑是什么?

我的理解是:每一种触发条件,都对应浏览器在合成(Compositing)阶段的一个实际需求——它需要把这个元素及其子树单独处理,不能混在普通文档流里一起渲染。

逐条来看:

position: relative/absolute/fixed + z-index 不为 auto

.box {
  position: relative;
  z-index: 1; /* 触发层叠上下文 */
}

z-index: auto 表示"不参与层叠上下文的建立,z 序由父上下文决定"。一旦设置了具体数值,浏览器需要知道:这个元素内部的子元素应该以谁为参照来叠放?答案就是"以这个元素为根,建立一个新的层叠上下文"。

z-index 的比较需要一个局部坐标系,这个元素就是那个坐标系的原点。

opacity < 1

.box {
  opacity: 0.5; /* 触发层叠上下文 */
}

这一条让很多人困惑。opacity 和叠放有什么关系?

关键在于浏览器的渲染流程:应用 opacity 时,浏览器需要把这个元素及其所有子元素先合成为一张完整的位图(纹理),然后整体降低透明度,再合入父层。

如果不建立独立的层叠上下文,每个子元素单独透明,视觉效果会完全不同——重叠区域会叠加透明度,看起来就乱了。

所以 opacity 必须建立独立上下文,让子树作为整体处理。这是视觉正确性的要求,不是设计偏好。

/* 验证这一点:如果 opacity 不建立层叠上下文 */
/* 两个互相重叠的子元素,在父元素 opacity: 0.5 时 */
/* 会出现重叠区域更透明的视觉 bug */
.parent { opacity: 0.5; }
.child-a { width: 100px; height: 100px; background: red; }
.child-b { width: 100px; height: 100px; background: blue; margin-top: -50px; }
/* 浏览器正确处理:先把 parent 的子树合成为一个整体,再应用 0.5 透明度 */

transform: 任何值(除 none)

.box {
  transform: translateX(10px); /* 触发层叠上下文 */
}

transform 会触发 GPU 合成层提升(Composite Layer Promotion)。元素被提升到独立的合成层之后,GPU 可以单独对这一层做变换,不必重新触发 Layout 和 Paint。

但独立合成层有一个前提:它的内部叠放顺序必须是确定的,否则 GPU 不知道该怎么合成。因此它必须建立独立的层叠上下文。

这也解释了为什么 transform: none 不触发——没有离开普通文档流的渲染路径,不需要独立上下文。

filter: 任何值(除 none)

.box {
  filter: blur(4px); /* 触发层叠上下文 */
}

opacity 类似,但更极端。filter 的效果(blur、drop-shadow 等)必须基于整个子树的合成结果才能计算。

比如 blur(4px) 需要获取该元素的像素边界,对边界外也做模糊扩散——这只有先把子树渲染成一张完整纹理,才能做到。如果子元素还在和外部文档流混排,这个效果根本没办法计算。

filter 建立层叠上下文,是滤镜特效在物理上可计算的前提。


一个帮助理解的心智模型

可以把层叠上下文想象成 Photoshop 里的图层组

根文档(顶层层叠上下文)
├── 普通元素(在这个组里按顺序叠放)
├── .box-a(opacity: 0.8)← 新建了一个图层组
│   ├── 子元素 1
│   └── 子元素 2
│   (子元素 2 和父上下文里的元素比 z-index 没有意义,它们在不同"组"里)
└── .box-b(z-index: 100)← 另一个图层组
    └── 子元素(z-index: 9999,也无法超过父上下文的 .box-a)

每个"图层组"内部自行排序,整体再参与上层的排序。子元素的 z-index 永远只在自己所在的"图层组"里生效。


面试常问版

属性 触发 BFC 触发层叠上下文
overflow: hidden/auto/scroll
display: flow-root
position: absolute/relative + z-index ≠ auto ✅(absolute/fixed)
opacity < 1 ✅(子树需整体合成)
transform ≠ none ✅(GPU 合成层提升)
filter ≠ none ✅(滤镜需整体像素计算)
display: flex/grid ❌(子项另说)
float ≠ none ✅(自身)

面试可能追问的核心逻辑

  • BFC 的本质:布局维度的隔离,解决 float、margin 折叠的"副作用渗透"问题
  • 层叠上下文的本质:合成维度的隔离,让需要独立处理的元素及其子树有确定的 z 序边界
  • 为什么 opacity/transform/filter 会触发:不是 CSS 规范的"任意规定",是浏览器渲染管线(Paint → Composite)在技术上的必然要求

延伸思考

研究这两个概念的过程中,我产生了一些新的疑问:

  1. will-change: transform 会提前触发合成层提升,但是否同时建立层叠上下文?(答案是:会,但这个"预建立"对布局有没有副作用?)
  2. 在 React 组件中,如果父组件用了 transform 做动画,子组件里的 Portal(比如 Modal)会受到层叠上下文的影响吗?(这在实际开发中是个坑)
  3. 现代 CSS 的 @layer 是否引入了新的层叠维度?它和 z-index 的关系是什么?

这些问题我还在继续探索,如果你有自己的理解,欢迎交流。


小结

BFC 和层叠上下文,都是浏览器在渲染时建立"边界"的机制。理解它们,我觉得最重要的不是记住"哪些属性触发",而是理解为什么要有这个边界

  • BFC:浮动和 margin 折叠在普通流里会"渗透",需要一个容器划定范围自管布局
  • 层叠上下文:opacity、transform、filter 等特效需要把子树整体处理,必须有确定的 z 序边界

记住规则可以应付面试,但理解背后的渲染逻辑,才能在遇到真实 bug 时有判断力。

参考资料

HTTP 缓存策略:新鲜度与速度的权衡艺术

作者 yuki_uix
2026年4月21日 10:05

在优化 Web 应用性能时,我发现一个有趣的矛盾:用户希望看到最新的内容,但同时又期望页面加载飞快。这个矛盾的解决方案,就藏在 HTTP 缓存机制中。

那么,HTTP 缓存到底是如何工作的?强缓存和协商缓存有什么区别?如何为不同类型的资源设置合适的缓存策略?

问题的起源

为什么需要缓存?最直接的原因是性能。网络请求的延迟远高于本地读取,尤其在移动网络环境下。如果每次访问都要重新下载所有资源,用户体验会很差。

但缓存又带来了新的问题:新鲜度。如果资源被缓存了,用户如何获取更新后的版本?

HTTP 缓存机制就是在这两个目标之间寻找平衡:既要快,又要新。

核心概念探索

1. 浏览器缓存的层级结构

在深入 HTTP 缓存之前,先了解浏览器的完整缓存体系:

浏览器请求资源的缓存查找顺序:

  1. Memory Cache(内存缓存)

    • 特点:最快,但容量小,tab 关闭即清空
    • 存储:当前页面的资源(图片、脚本、样式)
  2. Service Worker Cache

    • 特点:可编程,离线可用
    • 存储:开发者主动缓存的资源
  3. Disk Cache(磁盘缓存)

    • 特点:容量大,持久化
    • 存储:根据 HTTP 缓存头决定
  4. Push Cache(HTTP/2 推送缓存)

    • 特点:短暂存在,只在会话期间
    • 存储:服务器推送的资源
  5. 网络请求

    • 最后的选择:如果以上都没有,发起网络请求

今天我们主要关注的是 Disk Cache 层面的 HTTP 缓存。

2. 强缓存(Strong Cache)

强缓存是指浏览器直接从本地缓存读取资源,不发送任何网络请求到服务器。

Expires(HTTP/1.0)

HTTP/1.0 200 OK
Content-Type: text/css
Expires: Wed, 21 Oct 2026 07:28:00 GMT

/* CSS 内容 */

Expires 的问题:

  1. 使用的是绝对时间:如果服务器和客户端时间不同步,缓存会失效

  2. 优先级低于 Cache-Control:如果两者同时存在,Expires 会被忽略

Cache-Control(HTTP/1.1,推荐)

HTTP/1.1 200 OK
Content-Type: application/javascript
Cache-Control: max-age=31536000

/* JavaScript 内容 */

Cache-Control 常用指令

HTTP 响应头 + Cache-Control 指令详解

  1. max-age=<seconds>

    • 指定资源缓存的最大时长(相对时间,单位:秒)
    • Cache-Control: max-age=3600 // 缓存 1 小时
  2. no-cache

    • 不是"不缓存"!而是"需要验证"
    • 浏览器会缓存资源,但每次使用前必须向服务器验证是否过期
    • Cache-Control: no-cache
  3. no-store

    • 真正的"不缓存":浏览器不缓存,每次都重新请求
    • Cache-Control: no-store
  4. public

    • 允许中间代理(CDN)缓存
    • Cache-Control: public, max-age=86400
  5. private

    • 只允许浏览器缓存,中间代理不能缓存(如包含用户隐私信息的响应)
    • Cache-Control: private, max-age=3600
  6. immutable

    • 表示资源永远不会改变,即使用户刷新页面也不重新验证
    • Cache-Control: max-age=31536000, immutable
  7. must-revalidate

    • 缓存过期后必须向服务器验证,不能使用过期缓存
    • Cache-Control: max-age=3600, must-revalidate

常见组合

# 场景 1:永久缓存(适合带 hash 的静态资源)
Cache-Control: public, max-age=31536000, immutable

# 场景 2:不缓存(适合 HTML 入口文件)
Cache-Control: no-cache

# 场景 3:私密内容(适合用户个人信息)
Cache-Control: private, max-age=0, must-revalidate

# 场景 4:完全不存储(适合敏感数据)
Cache-Control: no-store

3. 协商缓存(Negotiation Cache)

当强缓存失效后,浏览器会发送请求到服务器,但可以通过协商来判断资源是否需要重新下载。

Last-Modified / If-Modified-Since

# 首次请求响应:
HTTP/1.1 200 OK
Last-Modified: Mon, 10 Jan 2026 10:00:00 GMT
Cache-Control: no-cache

/* 资源内容 */
# 再次请求时,浏览器携带:
GET /style.css HTTP/1.1
If-Modified-Since: Mon, 10 Jan 2026 10:00:00 GMT

# 如果资源未修改,服务器返回:
HTTP/1.1 304 Not Modified
# 没有响应体,浏览器使用本地缓存

# 如果资源已修改,服务器返回:
HTTP/1.1 200 OK
Last-Modified: Tue, 11 Jan 2026 14:30:00 GMT

/* 新的资源内容 */

Last-Modified 的局限性

问题 1:精度只到秒:如果文件在 1 秒内修改多次,无法检测到

问题 2:基于修改时间:即使文件内容没变,只是修改了时间戳(如重新编译),也会被认为是"已修改"

问题 3:某些服务器无法准确获取文件修改时间

ETag / If-None-Match(推荐)

ETag 是资源的唯一标识(通常是文件内容的 hash 值)。

# 首次请求响应:
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: no-cache

/* 资源内容 */
# 再次请求时,浏览器携带:
GET /app.js HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 如果 ETag 匹配(内容未改变),服务器返回:
HTTP/1.1 304 Not Modified

# 如果 ETag 不匹配(内容已改变),服务器返回:
HTTP/1.1 200 OK
ETag: "7f8c9d2e1a3b4c5d6e7f8g9h0i1j2k3l4m5n6o7p"

/* 新的资源内容 */

ETag vs Last-Modified

特性 ETag Last-Modified
精度 基于内容 hash,精度高 基于时间,精度到秒
优先级 高(如果同时存在,优先使用 ETag)
服务器开销 需要计算 hash,开销大 开销小
适用场景 内容频繁变化,需要精确控制 一般场景

4. 缓存决策流程

浏览器请求资源时的完整决策过程:

// 环境:浏览器内部逻辑
// 场景:缓存决策流程(伪代码)

function fetchResource(url) {
  // 1. 检查 Memory Cache
  if (memoryCache.has(url)) {
    return memoryCache.get(url);
  }
  
  // 2. 检查 Service Worker Cache
  if (serviceWorkerCache.has(url)) {
    return serviceWorkerCache.get(url);
  }
  
  // 3. 检查 Disk Cache(HTTP 缓存)
  const cached = diskCache.get(url);
  
  if (cached) {
    // 3.1 检查是否有 Cache-Control: no-store
    if (cached.headers['cache-control'].includes('no-store')) {
      // 不使用缓存,直接请求
      return fetchFromNetwork(url);
    }
    
    // 3.2 检查强缓存是否有效
    const maxAge = getCacheMaxAge(cached.headers);
    const age = Date.now() - cached.timestamp;
    
    if (age < maxAge) {
      // 强缓存有效,直接返回
      console.log('from disk cache');
      return cached.data;
    }
    
    // 3.3 强缓存失效,检查是否需要协商缓存
    if (cached.headers['cache-control'].includes('no-cache') || cached.headers.etag || cached.headers['last-modified']) {
      // 发起协商缓存请求
      return revalidateCache(url, cached);
    }
  }
  
  // 4. 没有缓存,发起网络请求
  return fetchFromNetwork(url);
}

function revalidateCache(url, cached) {
  const headers = {};
  
  // 添加协商缓存请求头
  if (cached.headers.etag) {
    headers['If-None-Match'] = cached.headers.etag;
  }
  if (cached.headers['last-modified']) {
    headers['If-Modified-Since'] = cached.headers['last-modified'];
  }
  
  const response = fetch(url, { headers });
  
  if (response.status === 304) {
    // 资源未修改,使用本地缓存
    console.log('304 Not Modified');
    return cached.data;
  }
  
  // 资源已修改,使用新内容并更新缓存
  return response.data;
}

用 Mermaid 图表表示:

graph TD
    A[请求资源] --> B{Memory Cache?}
    B -->|有| C[返回缓存]
    B -->|无| D{Service Worker?}
    D -->|有| C
    D -->|无| E{Disk Cache?}
    E -->|无| F[网络请求]
    E -->|有| G{no-store?}
    G -->|是| F
    G -->|否| H{强缓存有效?}
    H -->|是| C
    H -->|否| I{支持协商缓存?}
    I -->|否| F
    I -->|是| J[发起验证请求]
    J --> K{304?}
    K -->|是| C
    K -->|否| L[下载新资源]

实际场景思考

场景 1:SPA 应用的缓存策略

单页应用(SPA)通常有这样的文件结构:

dist/
├── index.html           # 入口文件
├── main.[hash].js       # 应用主逻辑
├── vendor.[hash].js     # 第三方库
├── style.[hash].css     # 样式文件
└── assets/
    └── logo.[hash].png  # 静态资源

推荐的缓存策略

// 环境:Nginx / Node.js 服务器
// 场景:为不同类型文件设置缓存

// 1. index.html:永远不缓存(或协商缓存)
// 原因:作为入口,必须获取最新版本来引用正确的 hash 文件
location = /index.html {
  add_header Cache-Control "no-cache";
  # 或者
  # add_header Cache-Control "no-store";
}

// 2. 带 hash 的资源文件:永久缓存
// 原因:文件名包含内容 hash,内容变化文件名就变,可以放心长缓存
location ~* .(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$ {
  # 如果文件名包含 hash
  if ($request_filename ~* .[a-f0-9]{8,}.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$) {
    add_header Cache-Control "public, max-age=31536000, immutable";
  }
}

// 3. 不带 hash 的资源:短期缓存 + 协商缓存
location ~* .(js|css)$ {
  add_header Cache-Control "public, max-age=3600";
  # 浏览器会自动处理 ETag/Last-Modified
}

Webpack 配置生成 hash 文件名

// 环境:Node.js
// 场景:Webpack 配置
// 依赖:webpack

module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css',
      chunkFilename: '[name].[contenthash:8].chunk.css',
    }),
  ],
};

// contenthash:基于文件内容生成 hash
// 只有内容改变,hash 才会变
// 用户访问 index.html 时,会看到:
// <script src="/main.a1b2c3d4.js"></script>
// 如果 main.js 内容改变,变成:
// <script src="/main.e5f6g7h8.js"></script>
// 浏览器会请求新文件,而不是使用旧的缓存

场景 2:强制用户更新资源

即使设置了正确的缓存策略,有时仍需要强制用户更新:

// 问题场景:
// 用户已经访问过旧版本,浏览器缓存了 index.html
// 即使部署了新版本,用户刷新页面仍然看到旧的 index.html
// 旧的 index.html 引用旧的 js 文件

// 解决方案 1:index.html 使用 no-cache(推荐)
// 每次都向服务器验证,确保获取最新版本

// 解决方案 2:index.html 添加版本号查询参数
// 通过修改 URL 强制浏览器请求新资源
<script src="/app.js?v=1.2.3"></script>

// 解决方案 3:使用 Service Worker 控制缓存
// Service Worker 可以主动清除旧缓存
self.addEventListener('activate', event => {
  const cacheWhitelist = ['v2'];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (!cacheWhitelist.includes(cacheName)) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

场景 3:开发环境 vs 生产环境的缓存差异

// 环境:Webpack DevServer
// 场景:开发环境禁用缓存

// 开发环境配置
module.exports = {
  devServer: {
    headers: {
      // 禁用缓存,确保每次都获取最新代码
      'Cache-Control': 'no-store',
    },
  },
};

// 为什么开发环境要禁用缓存?
// 1. 代码频繁修改,需要实时看到效果
// 2. 避免改了代码但浏览器使用旧缓存的困惑
// 3. 开发环境不关心性能,关心开发体验

// 生产环境配置(Nginx)
// 需要精细的缓存策略,平衡性能和新鲜度

场景 4:CDN 缓存失效

CDN 有自己的缓存层,如何处理?

// 问题:部署了新版本,但 CDN 仍然返回旧内容

// 解决方案 1:CDN Purge API(手动清除缓存)
// 大多数 CDN 提供了清除缓存的 API
// 例如 Cloudflare:
const response = await fetch('https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache', {
  method: 'POST',
  headers: {
    'X-Auth-Email': 'user@example.com',
    'X-Auth-Key': 'your-api-key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    files: [
      'https://example.com/style.css',
      'https://example.com/app.js',
    ],
  }),
});

// 解决方案 2:使用带 hash 的文件名(最佳实践)
// 文件内容变化 → hash 变化 → URL 变化 → CDN 缓存失效
// 这样就不需要手动清除 CDN 缓存了

// 解决方案 3:设置合适的 Cache-Control
// 对于 CDN,可以使用 s-maxage 单独控制 CDN 缓存时长
Cache-Control: public, max-age=3600, s-maxage=86400
// max-age:浏览器缓存 1 小时
// s-maxage:CDN 缓存 24 小时

场景 5:Cookie 与缓存

Cookie 会影响缓存行为:

// 问题:包含 Cookie 的请求默认不会被 CDN 缓存

// 请求:
GET /api/user HTTP/1.1
Cookie: session_id=abc123

// CDN 通常不会缓存这个响应,因为它可能包含用户特定的内容

// 解决方案 1:静态资源使用独立域名(Cookie-free domain)
// HTML:https://www.example.com  (可能有 Cookie)
// 静态资源:https://static.example.com (无 Cookie)

// 解决方案 2:使用 Vary 响应头
HTTP/1.1 200 OK
Vary: Cookie
Cache-Control: public, max-age=3600

// Vary: Cookie 告诉缓存服务器:
// 不同 Cookie 的请求应该分别缓存

知识点快速回顾

(30 秒版本)

Q: 什么是强缓存和协商缓存?

A: 强缓存是浏览器直接从本地读取资源,不发送请求到服务器,通过 Cache-Control(如 max-age)控制;协商缓存是浏览器向服务器验证资源是否过期,如果未过期返回 304,使用本地缓存,通过 ETag/Last-Modified 控制。

Q: Cache-Control 的常用指令有哪些?

A:

  • max-age=<seconds>:缓存时长
  • no-cache:需要验证(不是不缓存)
  • no-store:不缓存
  • public:允许 CDN 缓存
  • private:只允许浏览器缓存
  • immutable:资源不会变化

Q: ETag 和 Last-Modified 有什么区别?

A: ETag 基于内容 hash,精度高,优先级高,但服务器开销大;Last-Modified 基于修改时间,精度到秒,优先级低,开销小。如果两者都存在,优先使用 ETag。

(2 分钟版本)

Q: SPA 应用如何设置缓存策略?

A: 典型策略是:

  • index.htmlno-cache(每次验证,确保获取最新版本)
  • 带 hash 的资源(app.[hash].js):max-age=31536000, immutable(永久缓存)
  • 不带 hash 的资源:短期缓存(如 max-age=3600

原理是:index.html 作为入口必须最新,它引用的资源文件名包含 hash,内容变化时 hash 就变,URL 变了缓存自然失效。

Q: 为什么有些资源显示 "from disk cache",有些显示 "from memory cache"?

A: Memory Cache 是内存缓存,速度最快但容量小,tab 关闭即清空,通常缓存当前页面的资源;Disk Cache 是磁盘缓存,容量大、持久化,根据 HTTP 缓存头控制。浏览器会优先查找 Memory Cache,没有再查找 Disk Cache。

Q: no-cache 和 no-store 的区别?

A:

  • no-cache:浏览器会缓存资源,但每次使用前必须向服务器验证(协商缓存),如果服务器返回 304,使用本地缓存
  • no-store:完全不缓存,每次都重新下载

no-cache 的命名容易误解,它不是"不缓存",而是"缓存但需验证"。

Q: 304 状态码的完整流程是什么?

A:

  1. 浏览器发现强缓存过期(或设置了 no-cache)
  2. 发起请求,携带 If-None-Match(ETag)或 If-Modified-Since(时间戳)
  3. 服务器比对 ETag 或修改时间
  4. 如果资源未改变,返回 304 Not Modified(无响应体)
  5. 浏览器使用本地缓存

304 响应虽然也有网络请求,但没有响应体,节省了带宽。

Q: 如何强制用户更新缓存的资源?

A: 常见方法:

  1. 文件名加 hash(最佳):app.[contenthash].js
  2. URL 加版本号:style.css?v=1.2.3
  3. 设置 no-cache:每次验证
  4. CDN Purge:手动清除 CDN 缓存
  5. Service Worker:主动清除旧缓存

推荐第 1 种,因为它自动化、可靠、不需要手动操作。

有关 HTTP 缓存策略的高频关键概念

  • 强缓存 / 协商缓存
  • Cache-Control / Expires
  • ETag / If-None-Match
  • Last-Modified / If-Modified-Since
  • 304 Not Modified
  • max-age / no-cache / no-store
  • public / private / immutable
  • Memory Cache / Disk Cache
  • contenthash(Webpack)
  • CDN 缓存
  • Stale-While-Revalidate

容易踩的坑

  1. 混淆 no-cache 和 no-store:no-cache 会缓存但需验证,no-store 才是完全不缓存
  2. 忘记 index.html 也会被缓存:用户可能看到旧的 index.html,即使资源文件都更新了
  3. 过度依赖手动清除 CDN 缓存:应该使用带 hash 的文件名实现自动失效
  4. 静态资源域名包含 Cookie:Cookie 会阻止 CDN 缓存,应使用独立的无 Cookie 域名
  5. 开发环境忘记禁用缓存:导致改了代码但浏览器使用旧缓存

缓存策略决策树

资源类型?
├─ HTML 入口文件 → no-cache(或 no-store)
├─ 带 hash 的 JS/CSS/图片 → max-age=31536000, immutable
├─ 不带 hash 的静态资源 → max-age=3600(短期缓存)
├─ API 响应
│   ├─ 用户特定数据 → private, no-cache
│   ├─ 公共数据(不常变)→ public, max-age=60
│   └─ 实时数据 → no-store
└─ 字体文件 → public, max-age=31536000

小结

HTTP 缓存是 Web 性能优化的基石。理解强缓存和协商缓存的区别、Cache-Control 的各种指令、ETag 的工作原理,能帮助我们为不同类型的资源设置合适的缓存策略,在性能和新鲜度之间找到平衡。

这篇文章主要探讨了:

  • 浏览器缓存的层级结构
  • 强缓存(Cache-Control / Expires)
  • 协商缓存(ETag / Last-Modified)
  • 缓存决策流程
  • SPA 应用的缓存最佳实践
  • CDN 缓存处理

参考资料

❌
❌