浏览器缓存优先级

31 阅读19分钟

公众号:小博的前端笔记

核心原则:浏览器总是优先尝试使用最快、最接近的可用有效缓存,避免不必要的网络请求。

以下是浏览器缓存机制的详细优先级顺序(从最高到最低),以及关键细节:

1、Service Worker 缓存 (最高优先级)

  • 触发条件: 网站注册了 Service Worker,并且该 Service Worker 的 fetch 事件处理程序中有意拦截请求并返回缓存响应。

  • 机制:

    • Service Worker 作为浏览器和网络之间的代理运行。
    • fetch 事件中,你可以完全编程控制如何响应请求:可以返回 CacheStorage (caches API) 中的缓存、发起网络请求、构造新响应,甚至返回占位符。
    • 策略完全自定义: 你可以实现 CacheFirst(优先缓存)、NetworkFirst(优先网络)、StaleWhileRevalidate(快速返回缓存后更新)、NetworkOnly(仅网络)等任何策略。
  • 优先级最高原因: Service Worker 能拦截所有同源下的请求(包括页面本身、HTML、CSS、JS、图片、API 请求等)。它的 fetch 事件处理程序最先执行,如果它决定从自己的缓存中返回响应,浏览器将不会进行后续任何缓存检查或网络请求。

  • 面试要点: 强调其编程可控性最高拦截权。即使资源在 HTTP 缓存中过期,只要 Service Worker 决定返回缓存版本,浏览器就会使用它。它是实现离线应用、高级缓存策略(如后台更新)的核心。

2、Memory Cache (内存缓存)

  • 触发条件: 当前浏览会话(Tab 页)中已加载过的资源(如脚本、样式、图片)。通常是同一页面或之前页面加载的资源被再次请求。

  • 机制:

    • 资源被直接存储在系统内存 (RAM) 中。
    • 访问速度极快(比磁盘快几个数量级)。
    • 生命周期非常短:关闭 Tab 页通常会被清除。浏览器在内存紧张时也会主动清理。
    • 通常用于存储较小高频访问的资源(如当前页面引用的 JS、CSS、小图标)。大的图片/视频可能不会被放入内存缓存,或优先被清除。
  • 优先级原因: 速度最快。如果资源在内存中存在且有效(未过期),浏览器会毫不犹豫地使用它。

  • 面试要点: 强调速度优势临时性。它是提升页面二次加载速度(尤其是刷新)的关键。开发者无法直接控制哪些资源进入内存缓存。

3、HTTP Cache (Disk Cache / 磁盘缓存)

  • 触发条件: 资源响应头中包含有效的缓存指令(如 Cache-Control, Expires, ETag, Last-Modified)。

  • 机制: 这是最核心、最常用、开发者可控度最高的缓存机制。

    • 资源被存储在硬盘上。

    • 容量大,生命周期长(按策略,可能跨会话、跨 Tab)。

    • 强缓存 (Freshness):

      • 浏览器检查 Cache-Control (优先级高) 和 Expires 头部,判断资源是否在有效期内 (max-age, s-maxage)。
      • 如果有效期内 (fresh),浏览器直接从磁盘加载资源,完全不发送任何网络请求! (状态码 200 from disk cache / from prefetch cache)。
    • 协商缓存 (Validation):

      • 如果强缓存失效 (stale),浏览器会发起一个条件请求 (Conditional Request)

      • 请求头带上之前响应中的 ETag 值 (If-None-Match) 或 Last-Modified 时间 (If-Modified-Since)。

      • 服务器检查资源:

        • 如果未修改:返回 304 Not Modified (空 body),浏览器从 HTTP 缓存加载资源(状态码 200 from disk cache304)。
        • 如果已修改:返回 200 OK 和完整的新资源,浏览器使用新资源并更新缓存
    • 关键头部:

      • Cache-Control: 核心指令 (max-age=3600, no-cache, no-store, public, private, immutable, must-revalidate, stale-while-revalidate等)。
      • Expires (HTTP/1.0,被 Cache-Control 覆盖):绝对过期时间。
      • ETag / Last-Modified: 用于协商缓存验证。
      • Vary: 指定缓存变体(如按 User-Agent, Accept-Encoding 缓存不同版本)。
  • 优先级原因: 速度较快(比网络快),容量大,持久性强,是开发者主要优化的目标。当内存缓存没有命中(比如首次访问、内存已清除),浏览器就会检查 HTTP 缓存。

  • 面试要点: 这是必考重点!必须清晰区分强缓存(不发请求)和协商缓存(发条件请求,可能返回 304)。深入理解 Cache-Control 常用指令的含义和组合。强调 no-store (完全不缓存) 和 no-cache (总是验证) 的区别。解释 ETag (通常更精确) 和 Last-Modified 的优缺点。

4、Push Cache (HTTP/2 Server Push 缓存) (最低优先级且特殊)

  • 触发条件: 服务器使用 HTTP/2 的 Server Push 功能主动推送资源给浏览器。

  • 机制:

    • 资源在服务器主动推送时被临时存储
    • 生命周期极短:通常只存在于当前 HTTP/2 连接期间。关闭连接或打开新连接都会失效。
    • 同源限制宽松:即使跨域推送的资源也可能被存储(但遵循同源策略使用)。
    • 非常规获取方式: 资源必须是被 Server Push 推下来的,不能通过常规的 GET 请求去“读取” Push Cache。
    • 低优先级: 当浏览器需要某个资源时,如果它恰好在 Push Cache 中且有效(连接未关闭),可能会使用它。但它非常脆弱,通常作为优化手段(减少关键资源 RTT)。
  • 优先级原因: 存在性依赖特殊条件(HTTP/2 + Server Push),且生命周期短。如果其他缓存(Memory/HTTP)有有效副本,它们会被优先使用。Push Cache 是最后一道防线。

  • 面试要点: 说明其特殊性和临时性。知道它是 HTTP/2 的优化特性,但在实际缓存决策中作用有限且优先级最低。不是所有浏览器都支持或实现完善。

关键决策流程与优先级体现 (浏览器视角):

  1. 请求发起: 浏览器需要加载一个资源 (URL)。

  2. Service Worker 拦截?

    • 是: 执行 Service Worker 的 fetch 事件处理程序。

      • 如果处理程序返回了缓存响应(无论来自 CacheStorage 还是其他地方):直接使用该响应,流程结束。 (最高优先级体现)
      • 如果处理程序决定忽略缓存并直接 fetch() 网络请求:则流程继续向下。
  3. Memory Cache 检查?

    • 浏览器检查当前内存中是否有该资源的有效副本。
    • 是且有效: 直接使用内存缓存,流程结束。 (次高优先级体现)
    • 否 或 无效: 流程继续。
  4. HTTP Cache (Disk Cache) 检查?

    • 浏览器检查硬盘上是否有该资源的缓存副本及其有效性。

    • 强缓存有效 (fresh): 直接使用磁盘缓存,不发任何请求,流程结束。 (核心缓存层)

    • 强缓存失效 (stale) 但有验证器 (ETag/Last-Modified):

      • 浏览器发起一个带验证头 (If-None-Match/If-Modified-Since)条件请求到服务器。
      • 服务器响应 304 Not Modified浏览器使用磁盘缓存中(已失效但未变)的副本,流程结束。 (协商缓存成功)
      • 服务器响应 200 OK (新内容):浏览器使用新响应,更新 HTTP 缓存,流程结束。
    • 无缓存 或 缓存无效且无验证器 或 no-store 流程继续。

  5. Push Cache 检查?(如果存在 HTTP/2 连接且之前有推送)

    • 浏览器检查当前连接关联的 Push Cache 中是否有该资源。
    • 是且连接有效: 可能会使用它(但非常不可靠,优先级最低)。 (最低优先级体现)
    • 否 或 无效: 流程继续。
  6. 网络请求:

    • 以上所有缓存层均未提供有效响应。
    • 浏览器发起一个常规的 GET 请求到服务器。
    • 服务器返回响应 (200 OK)。
    • 浏览器根据响应头 (Cache-Control 等) 决定是否以及如何缓存这个新响应(存入 HTTP Cache / Memory Cache)。
    • 使用响应内容。

面试回答技巧与要点总结:

  1. 结构化阐述: 按优先级顺序清晰列出层级:Service Worker -> Memory Cache -> HTTP Cache (强缓存 -> 协商缓存) -> Push Cache -> Network。

  2. 强调关键点:

    • Service Worker 的绝对控制权:它最先拦截,可以决定一切。
    • Memory Cache 的速度优势与临时性
    • HTTP Cache 是核心战场:深入解释 Cache-Control (max-age, no-cache, no-store)、强缓存(不发请求)与协商缓存(发条件请求,304 vs 200)的本质区别。这是面试官最可能深挖的地方。
    • Push Cache 的特殊性和低优先级
  3. 解释流程: 简述浏览器决策链:“浏览器会依次检查 Service Worker 是否拦截并返回缓存 -> 检查内存缓存是否有效 -> 检查 HTTP 强缓存是否有效 -> 如果强缓存失效则发条件请求检查协商缓存 -> 最后尝试 Push Cache -> 都失败才发网络请求”。

  4. 举例说明: 可以结合常见指令举例:

    • Cache-Control: max-age=3600, public 表示资源可以被任何缓存(浏览器、CDN)存储,并且在 3600 秒内,浏览器会直接从内存或磁盘加载它,不发请求(强缓存)。”
    • Cache-Control: no-cache 不表示不缓存,而是每次使用前必须向服务器验证(发带 ETag/Last-Modified 的条件请求),如果服务器返回 304 则用缓存(协商缓存)。”
    • Cache-Control: no-store 才是真正的不缓存,每次都必须从网络获取。”
  5. 提到启发式缓存: 如果响应没有明确指定 ExpiresCache-Control: max-age,但提供了 Last-Modified,浏览器可能会根据 Last-Modified 和当前时间的差值(通常取该差值的 10% 作为 max-age)进行启发式缓存。这是一个常被忽略但重要的细节。

  6. 清晰区分状态码/来源:

    • 200 OK (from memory cache) / 200 OK (from disk cache):强缓存命中。
    • 304 Not Modified:协商缓存验证成功(服务器未修改)。
    • 200 OK (无特殊标注):来自网络请求。
  7. 结合实际优化: 最后可以提一下理解这些对性能优化的意义:如何设置合理的缓存策略减少请求、加快加载速度(静态资源长缓存+版本控制/指纹、API 谨慎缓存等)。

5、Service Worker 缓存是什么 如何实现

Service Worker 缓存是一种由 JavaScript 编程控制的浏览器缓存机制,它是实现离线应用、后台同步、推送通知以及高级缓存策略的核心技术。它赋予了开发者对网络请求和响应的精细控制权,使其优先级凌驾于传统的 HTTP 缓存和内存缓存之上。

核心概念:

  1. 独立的 JavaScript Worker:

    • Service Worker 是一个运行在浏览器后台的独立 JavaScript 线程。
    • 它与主页面 JavaScript(运行在 Window 上下文)完全隔离,不能直接访问 DOM。
    • 通过 postMessage API 与页面进行通信。
  2. 代理网络请求:

    • Service Worker 的核心功能是充当位于浏览器和网络之间的代理。
    • 它可以拦截、修改、响应其作用域 (scope) 内的所有网络请求(包括页面导航请求、HTML、CSS、JS、图片、API 调用等)。
  3. 事件驱动:

    • 其生命周期和行为由一系列事件驱动:

      • install: Service Worker 首次安装或更新时触发。通常在此事件中进行资源预缓存
      • activate: Service Worker 被激活,开始控制其作用域内的页面时触发。通常在此事件中进行旧缓存清理
      • fetch: 每当作用域内的页面发起网络请求时触发。开发者可以在此事件中拦截请求并决定如何响应(从缓存返回、转发到网络、构造新响应等)。
      • message: 用于接收来自页面或其他 Service Worker 的消息。
      • sync / push: 用于后台同步和推送通知(需要额外配置)。
  4. 作用域 (scope):

    • 定义 Service Worker 可以控制哪些页面。默认是其脚本文件所在的目录及其子目录。
    • 注册时可以通过 scope 选项指定更宽或更窄的范围(必须在脚本文件所在路径下)。
  5. 缓存存储 (CacheStorage / caches):

    • Service Worker 使用 CacheStorage API(通过全局的 caches 对象访问)来存储和管理缓存。
    • 缓存存储在 Cache 对象中,每个 Cache 可以存储大量的 Request/Response 对。
    • 开发者可以创建多个命名缓存(如 v1-static-assets, v2-api-responses),便于管理和版本控制。
  6. 生命周期管理:

    • 安装 (Installing): 下载并解析 Service Worker 脚本。
    • 等待 (Waiting): 如果当前有活动的旧版 Service Worker 控制着页面,新安装的 Service Worker 会进入此状态,等待接管(除非调用 skipWaiting())。
    • 激活 (Activating): 新 Service Worker 获得控制权,触发 activate 事件。旧 Service Worker 及其控制的页面将被停用。
    • 活动 (Activated): 控制页面,处理 fetch 等事件。
    • 冗余 (Redundant): 被新版本替换或安装失败。

如何实现 Service Worker 缓存 (核心步骤):

  1. 注册 Service Worker:

    • 在主页面 JavaScript 中注册 Service Worker 脚本文件 (sw.js)。
    // main.js (或你的入口脚本)
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', function() {
        navigator.serviceWorker.register('/sw.js', { scope: '/' }) // 注册 sw.js,作用域为根目录 '/'
          .then(function(registration) {
            console.log('ServiceWorker 注册成功,作用域: ', registration.scope);
          })
          .catch(function(error) {
            console.log('ServiceWorker 注册失败: ', error);
          });
      });
    }
    
  2. 编写 Service Worker 脚本 (sw.js) - 预缓存 (在 install 事件中):

    • install 事件中,打开一个缓存,并将需要预加载的关键静态资源(确保应用外壳能离线工作)添加到缓存中。
    • 使用 event.waitUntil() 确保安装过程直到缓存操作完成才结束。
    // sw.js
    const CACHE_NAME = 'my-site-cache-v1'; // 使用版本号便于后续更新
    const urlsToCache = [
      '/',
      '/index.html',
      '/styles/main.css',
      '/scripts/main.js',
      '/images/logo.png'
    ];
    ​
    self.addEventListener('install', function(event) {
      // 执行安装步骤:创建缓存并添加资源
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then(function(cache) {
            console.log('已打开缓存');
            return cache.addAll(urlsToCache); // 缓存指定资源列表
          })
      );
    });
    
  3. 编写 Service Worker 脚本 (sw.js) - 清理旧缓存 (在 activate 事件中):

    • activate 事件中,清理掉不属于当前版本的旧缓存。
    • 使用 event.waitUntil() 确保清理操作完成。
    self.addEventListener('activate', function(event) {
      const cacheWhitelist = [CACHE_NAME]; // 保留当前版本缓存
    ​
      event.waitUntil(
        caches.keys().then(function(cacheNames) {
          return Promise.all(
            cacheNames.map(function(cacheName) {
              if (cacheWhitelist.indexOf(cacheName) === -1) { // 不在白名单中的缓存
                return caches.delete(cacheName); // 删除旧缓存
              }
            })
          );
        })
      );
    });
    
  4. 编写 Service Worker 脚本 (sw.js) - 拦截请求 & 响应 (在 fetch 事件中):

    • 这是实现缓存策略的核心!fetch 事件允许你拦截所有作用域内的请求。
    • 你可以实现各种策略来决定如何响应请求。以下是一些常见策略的代码示例:

    策略 1: 缓存优先 (Cache First - 适合静态资源)

    self.addEventListener('fetch', function(event) {
      event.respondWith(
        caches.match(event.request) // 尝试在缓存中查找匹配的请求
          .then(function(response) {
            // 缓存命中,返回缓存响应
            if (response) {
              return response;
            }
            // 缓存未命中,发起网络请求
            return fetch(event.request)
              .then(function(networkResponse) {
                // 可选:将新获取的资源添加到缓存中 (动态缓存)
                // 注意:通常只缓存 GET 请求且成功的响应 (200)
                if (event.request.method === 'GET' && networkResponse.status === 200) {
                  const responseToCache = networkResponse.clone(); // 克隆响应(流只能读取一次)
                  caches.open(CACHE_NAME)
                    .then(function(cache) {
                      cache.put(event.request, responseToCache);
                    });
                }
                return networkResponse;
              });
          })
      );
    });
    

    策略 2: 网络优先 (Network First - 适合需要最新数据的请求)

    self.addEventListener('fetch', function(event) {
      event.respondWith(
        fetch(event.request) // 优先尝试网络请求
          .then(function(networkResponse) {
            // 网络请求成功,返回响应并可选更新缓存
            if (event.request.method === 'GET' && networkResponse.status === 200) {
              const responseToCache = networkResponse.clone();
              caches.open(CACHE_NAME)
                .then(function(cache) {
                  cache.put(event.request, responseToCache);
                });
            }
            return networkResponse;
          })
          .catch(function(error) {
            // 网络请求失败,尝试从缓存中获取
            return caches.match(event.request);
          })
      );
    });
    

    策略 3: 仅缓存 (Cache Only) / 仅网络 (Network Only) / 更新后重新验证 (Stale-While-Revalidate)

    • 这些策略的实现逻辑类似,根据需求调整 caches.matchfetch 的顺序以及处理逻辑即可。
  5. 处理更新:

    • 当用户访问页面时,浏览器会自动检查 sw.js 是否有更新(字节比对)。

    • 如果有更新,会下载新脚本并触发新 Service Worker 的 install 事件(旧 Service Worker 仍在运行)。

    • 新 Service Worker 默认进入 waiting 状态,直到所有被旧 Service Worker 控制的页面都关闭(或用户强制刷新)。

    • 为了强制新 Service Worker 立即接管,可以在新 Service Worker 的 install 事件中调用 self.skipWaiting()

      self.addEventListener('install', function(event) {
        self.skipWaiting(); // 强制新 SW 立即激活
        event.waitUntil(...);
      });
      
    • activate 事件中清理旧缓存(如前面所示)。

关键注意事项 & 最佳实践:

  1. HTTPS 或 localhost: Service Worker 只能在 HTTPS 网站或 localhost 上运行(开发环境),这是出于安全考虑。

  2. 作用域 (scope): 确保 scope 设置正确。Service Worker 只能控制其脚本文件所在路径及其子路径下的页面。

  3. 缓存策略选择: 根据资源类型选择合适的策略:

    • 静态资源(CSS, JS, 图片等): 通常使用 缓存优先 (Cache First) + 长 max-age HTTP 缓存 + 文件名哈希(内容指纹)实现“永久缓存”。Service Worker 确保即使 HTTP 缓存过期也能离线使用。
    • 需要频繁更新的数据(API 响应、用户内容): 通常使用 网络优先 (Network First)更新后重新验证 (Stale-While-Revalidate) ,优先获取最新数据,同时提供缓存作为后备或加速。
    • 关键页面(HTML): 策略需谨慎。网络优先 + 缓存后备能保证内容更新,但也可能造成离线不可用。缓存优先能保证离线可用,但可能展示旧内容。通常结合应用外壳 (App Shell) 模型。
  4. 缓存管理:

    • 版本控制: 每次更新静态资源或 Service Worker 逻辑时,务必更新缓存名称 (CACHE_NAME) ,并在 activate 事件中清理旧缓存。
    • 限制缓存大小: Service Worker 缓存没有自动大小限制(不同于 HTTP 缓存)。需要自行实现逻辑(如 caches.keys() + cache.entries() 计算大小,删除最旧/最少使用的缓存项)。
    • 避免缓存过多: 只缓存必要的资源。预缓存关键资源,动态缓存按需添加。
  5. 调试:

    • Chrome DevTools -> Application -> Service Workers: 查看注册状态、调试代码、强制更新、模拟离线状态、检查 CacheStorage。
    • Chrome DevTools -> Network: 查看请求是否被 Service Worker 拦截(Size 列显示 (ServiceWorker)),状态码为 200 (from ServiceWorker)
  6. 用户体验:

    • 离线页面:fetch 事件中,如果网络和缓存都失败,可以返回一个预缓存的离线页面 (offline.html)。
    • 更新提示: 监听 registration 对象的 updatefound 事件和 installing worker 的状态变化,通知用户有更新可用,并引导刷新页面以激活新 Service Worker。

总结:

实现 Service Worker 缓存的核心步骤是:注册 -> 安装时预缓存关键资源 -> 激活时清理旧缓存 -> 在 fetch 事件中实现缓存策略拦截请求并返回响应。它提供了无与伦比的灵活性和控制力,是构建快速、可靠、离线可用的现代 Web 应用(PWA)的基石。在面试中,清晰地阐述其核心概念、生命周期、事件驱动模型(特别是 install, activate, fetch)以及如何实现不同的缓存策略,能充分展示你对高级浏览器缓存机制的理解。

6、浏览器是如何决定哪些资源缓存到本地哪些资源缓存到内存呢

浏览器决定将资源缓存到磁盘(Disk Cache / HTTP Cache) 还是内存(Memory Cache) 是一个复杂的动态决策过程,涉及多种因素。开发者无法直接控制,但理解其逻辑有助于优化缓存策略。以下是关键决策因素:

一、核心决策因素

1. 资源类型与大小
存储位置典型资源类型原因
内存缓存- 小体积资源(JS、CSS、小图标) - 当前页面高频访问的资源(如首屏图片、核心JS)内存读取速度极快,适合快速响应当前页面的重复请求。 内存空间有限,优先存高频小资源。
磁盘缓存- 大体积资源(大图、视频、字体文件) - 低频访问的静态资源 - 强缓存策略明确的资源(max-age较长)磁盘空间大(GB级),适合存大文件。 持久化存储,可跨会话使用。
2. 缓存策略(HTTP头部)
  • Cache-Control 优先级最高

    • 若资源标记为 no-store不缓存
    • 若为 no-cache缓存但每次需验证(可能存内存或磁盘)。
    • 若为 max-age=3600按策略缓存(磁盘优先,大资源存磁盘)。
  • 启发式缓存:无明确过期时间时,浏览器根据 Last-Modified 时间推算有效期(如 (Date - Last-Modified) * 0.1),通常存磁盘。

3. 访问频率与时效性
行为存储倾向案例
同一页面内多次请求内存缓存首页加载后,脚本二次请求(如异步加载模块)直接从内存读取。
跨页面/会话重复请求磁盘缓存站点LOGO图片在多页面间共享,从磁盘读取。
短时效资源内存缓存实时性较高的API响应(如股票数据),短暂存内存。
4. 浏览器内部算法
  • LRU(Least Recently Used)算法

    • 内存缓存:空间紧张时优先淘汰最久未使用的小资源。
    • 磁盘缓存:自动清理过期资源,按访问频率保留常用资源。
  • 分区限制

    • 内存缓存:单进程/Tab独立(Chrome),关闭Tab即释放。
    • 磁盘缓存:全局共享,受操作系统磁盘空间限制。

二、开发者视角的优化建议

1. 利用HTTP头部引导缓存
# 引导存磁盘(大文件/长期缓存)
Cache-Control: public, max-age=31536000, immutable
​
# 引导存内存(小文件/高频资源)
Cache-Control: max-age=600, must-revalidate

:浏览器最终决策权仍在其内部算法。

2. 资源分割策略
  • 高频小资源:合并为单文件(如Webpack打包的vendor.js)→ 易被内存缓存。
  • 低频大资源:单独拆分(如视频、字体)→ 避免挤占内存缓存。
3. 避免破坏缓存的行为
  • 随机查询参数image.png?v=123 会被视为新资源,导致缓存失效。
  • 动态内容无缓存头:API响应未设Cache-Control → 不缓存或短时内存缓存。

三、缓存位置验证(开发者工具)

Chrome DevTools 中查看:

  1. Network面板 → 查看资源请求的 Size 列:

    • (memory cache)内存缓存
    • (disk cache)磁盘缓存
  2. Application面板

    • Cache Storage:查看Service Worker缓存。
    • Cache:查看HTTP磁盘缓存内容(需手动刷新)。

四、特殊场景处理

场景缓存行为
无痕模式内存缓存可用,磁盘缓存禁用(关闭标签后清除所有缓存)。
页面刷新(硬刷新)跳过内存缓存,强制检查磁盘缓存和网络(Cache-Control: max-age仍有效)。
Service Worker拦截优先级最高,可覆盖内存/磁盘缓存(如fetch事件返回自定义响应)。

总结:浏览器缓存决策逻辑

20250703_961fc7

核心原则: 浏览器以性能最优为目标,综合资源类型、大小、访问频率、HTTP策略动态分配存储位置。开发者应通过合理的缓存头部和资源优化策略,间接引导浏览器缓存行为。