Skip to content

前端缓存机制:从"重复请求"到"瞬间加载"的性能跃迁

在前端性能优化中,"减少重复请求" 是提升加载速度的核心手段之一——用户第二次访问页面时,如果所有资源都需要重新从服务器下载,不仅浪费带宽,还会让加载时间翻倍。而缓存机制就像" 本地仓库",把第一次请求的资源存起来,下次直接取用,从而实现"瞬间加载"。

前端缓存主要分为两大体系:HTTP缓存(浏览器内置的基础缓存)和Service Worker (可编程的高级缓存)。本文将从原理到实践,详解这两种缓存的应用场景和实现方式,帮你构建"高效、可控"的缓存策略。

一、HTTP缓存:浏览器的"自动记忆"功能

HTTP缓存是浏览器与服务器通过HTTP协议约定的缓存规则,无需前端代码干预即可自动工作,是最基础也最常用的缓存方式。它的核心逻辑是: 对于重复的资源请求,浏览器先查本地缓存,若缓存有效则直接使用,否则才向服务器请求

根据"是否需要向服务器确认缓存有效性",HTTP缓存分为强缓存协商缓存

1. 强缓存:直接使用本地缓存,不发请求

强缓存是"最高效"的缓存方式:浏览器判断缓存未过期时,直接从本地读取资源,不与服务器发生任何交互(连请求都不发),加载速度可提升至毫秒级。

它通过响应头中的ExpiresCache-Control字段控制。

(1)Expires:基于绝对时间的缓存(较旧)

Expires是HTTP/1.0定义的字段,值为一个绝对时间(如Expires: Wed, 04 Aug 2025 12:00:00 GMT),表示"该资源在这个时间前有效"。

工作流程

  • 第一次请求:服务器返回资源和Expires时间,浏览器缓存资源和时间。
  • 第二次请求:浏览器对比当前时间与Expires,若未过期则直接用缓存(状态码显示from memory cachefrom disk cache)。

缺点:依赖客户端时间(若用户修改本地时间,可能导致缓存失效或过期缓存被使用),现在已逐渐被Cache-Control替代。

(2)Cache-Control:基于相对时间的缓存(推荐)

Cache-Control是HTTP/1.1定义的字段,值为相对时间(如max-age=3600),表示"资源在请求成功后3600秒(1小时)内有效",优先级高于 Expires

常用指令

  • max-age=xxx:缓存有效时长(秒),核心指令。
  • public:允许所有中间节点(如CDN)缓存该资源。
  • private:仅允许浏览器缓存(中间节点不缓存),适用于用户私有数据(如带登录态的页面)。
  • no-cache:不使用强缓存,但可使用协商缓存(需要向服务器确认有效性)。
  • no-store:完全不缓存(每次都从服务器请求)。

示例

http
// 服务器响应头:该CSS文件缓存1小时
Cache-Control: public, max-age=3600

工作流程

  • 第一次请求:服务器返回资源和Cache-Control: max-age=3600,浏览器缓存资源,并记录"缓存创建时间"。
  • 第二次请求:计算"当前时间 - 缓存创建时间",若小于3600秒,则直接用缓存(不发请求)。

通俗比喻

强缓存像超市的"保质期":

  • Expires是"截止日期:2025年8月4日12点"(绝对时间)。
  • Cache-Control: max-age=3600是"开封后1小时内食用"(相对时间,更可靠)。
  • 未过保质期的商品(缓存),直接拿起来就用,不用问收银员(服务器)。

2. 协商缓存:问服务器"缓存还能用吗?"

当强缓存过期(或设置了no-cache),浏览器会向服务器发送请求,询问"本地缓存是否仍有效" ——这就是协商缓存。服务器通过对比资源标识,决定返回"新资源"或"缓存有效"的信号。

它通过Last-Modified/If-Modified-SinceETag/If-None-Match这两组字段实现。

(1)Last-Modified / If-Modified-Since:基于修改时间的验证

  • Last-Modified:服务器在响应头中返回资源的"最后修改时间"(如Last-Modified: Wed, 04 Aug 2024 10:00:00 GMT)。
  • If-Modified-Since:浏览器下次请求时,将Last-Modified的值放在该请求头中,告诉服务器"我本地缓存的最后修改时间是这个"。

工作流程

  1. 第一次请求:服务器返回资源,响应头带Last-Modified: T1
  2. 第二次请求:浏览器发送请求,请求头带If-Modified-Since: T1
  3. 服务器对比:
    • 若资源在T1后未修改:返回304 Not Modified(不返回资源体),浏览器用本地缓存。
    • 若资源已修改:返回200 OK和新资源,浏览器更新缓存。

(2)ETag / If-None-Match:基于唯一标识的验证

Last-Modified有局限性(如资源内容未变但修改时间变了,会导致误判),而ETag是资源的"唯一指纹"(由服务器根据资源内容生成,如哈希值),更精准。

  • ETag:服务器返回资源的唯一标识(如ETag: "abc123")。
  • If-None-Match:浏览器下次请求时,将ETag的值放在该请求头中,告诉服务器"我本地缓存的标识是这个"。

工作流程

  1. 第一次请求:服务器返回资源,响应头带ETag: "v1"
  2. 第二次请求:浏览器发送请求,请求头带If-None-Match: "v1"
  3. 服务器对比:
    • 若资源标识仍为"v1"(未修改):返回304 Not Modified,浏览器用缓存。
    • 若标识变为"v2"(已修改):返回200 OK和新资源,浏览器更新缓存。

通俗比喻

协商缓存像去图书馆借书:

  • Last-Modified是书的"最后修订日期",你问管理员:"这本书在2024年8月4日后改过吗?"
  • ETag是书的"版本号",你问管理员:"我手里的是v1版,有更新的v2版吗?"
  • 管理员说"没更新"(304),你就用自己手里的书;说"有更新"(200),就换本新书。

3. HTTP缓存的实际应用:静态资源缓存策略

不同类型的资源(HTML、CSS、JS、图片)适合不同的缓存策略,以下是实战配置:

(1)长期缓存:图片、字体、第三方库(不常变的资源)

  • 策略:强缓存+文件名哈希(如logo.a1b2c3.webp
  • 配置:Cache-Control: public, max-age=31536000(缓存1年)
  • 原理:资源内容变化时,文件名哈希会变(如logo.d4e5f6.webp),浏览器会认为是新资源,自动请求最新版本。

(2)协商缓存:HTML文件(可能频繁更新)

  • 策略:禁用强缓存,启用协商缓存
  • 配置:Cache-Control: no-cache(不使用强缓存)+ ETagLast-Modified
  • 原理:每次请求都问服务器"HTML是否更新",确保用户看到最新内容。

(3)不缓存:接口数据、实时内容

  • 策略:完全不缓存
  • 配置:Cache-Control: no-store
  • 原理:每次都从服务器获取最新数据(如用户余额、实时新闻)。

Nginx配置示例(实现上述策略)

nginx
# nginx.conf
server {
  # 图片、字体等:长期强缓存(1年)
  location ~* \.(png|jpg|jpeg|webp|avif|svg|woff2)$ {
    expires 1y; # 等同于Cache-Control: max-age=31536000
    add_header Cache-Control "public";
  }

  # CSS、JS:长期强缓存(带哈希的文件名)
  location ~* \.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public";
  }

  # HTML:协商缓存
  location ~* \.html$ {
    add_header Cache-Control "no-cache"; # 不使用强缓存
    add_header ETag "$request_filename$mtime"; # 基于文件修改时间生成ETag
  }

  # API接口:不缓存
  location ~* /api/ {
    add_header Cache-Control "no-store";
  }
}

二、Service Worker:前端可控的"超级缓存"

HTTP缓存虽然强大,但控制权在浏览器和服务器手中(前端无法通过代码干预)。而Service Worker是运行在浏览器后台的独立线程,能* 拦截请求、自定义缓存逻辑*,支持离线访问、精准缓存更新等高级功能,是PWA(渐进式Web应用)的核心技术之一。

1. Service Worker的工作原理:像"代理服务器"一样工作

Service Worker的生命周期和工作流程如下:

  1. 注册(Register):前端代码调用navigator.serviceWorker.register(),告诉浏览器Service Worker脚本的地址。
  2. 安装(Install):浏览器下载并执行脚本,此时可缓存静态资源(如CSS、JS、图片)。
  3. 激活(Activate):安装完成后激活,可清理旧缓存(避免缓存冲突)。
  4. 拦截请求(Fetch):激活后,所有符合作用域的请求会被Service Worker拦截,可自定义返回"缓存资源"或"服务器资源"。
  5. 更新(Update):当Service Worker脚本内容变化时,会触发重新安装,完成后激活新脚本。

2. 核心能力:离线访问与精准缓存

(1)离线访问:没网也能打开页面

通过在安装阶段缓存关键资源,Service Worker可在离线时返回缓存内容,让页面在无网络环境下仍能访问(如离线文档、APP外壳)。

(2)自定义缓存策略:比HTTP缓存更灵活

常见的缓存策略有:

  • 缓存优先(Cache First):先查缓存,没有再请求服务器(适合不常变的静态资源)。
  • 网络优先(Network First):先请求网络,失败再用缓存(适合接口数据)。
  • 缓存网络竞速(Cache & Network Race):同时查缓存和网络,谁快用谁(平衡速度和新鲜度)。

3. 实战:用Service Worker实现静态资源缓存

步骤1:注册Service Worker(前端代码)

在页面加载时注册Service Worker脚本(通常在index.js中):

javascript
// 检查浏览器是否支持Service Worker
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        try {
            // 注册Service Worker脚本(scope表示作用域,控制该路径下的所有请求)
            const registration = await navigator.serviceWorker.register('/sw.js', {scope: '/'});
            console.log('Service Worker注册成功');
        } catch (err) {
            console.log('Service Worker注册失败:', err);
        }
    });
}

步骤2:编写Service Worker脚本(sw.js)

实现"安装时缓存静态资源+请求时优先用缓存"的逻辑:

javascript
// 缓存名称(版本号,更新缓存时修改)
const CACHE_NAME = 'my-app-cache-v1';
// 需要缓存的静态资源列表
const ASSETS_TO_CACHE = [
    '/',
    '/index.html',
    '/css/style.css',
    '/js/main.js',
    '/images/logo.webp',
    '/offline.html' // 离线时显示的页面
];

// 1. 安装阶段:缓存静态资源
self.addEventListener('install', (event) => {
    // 等待缓存完成后再结束安装
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(ASSETS_TO_CACHE)) // 缓存所有指定资源
            .then(() => self.skipWaiting()) // 跳过等待,直接激活新的Service Worker
    );
});

// 2. 激活阶段:清理旧缓存
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(name => {
                    // 删除非当前版本的缓存
                    if (name !== CACHE_NAME) {
                        return caches.delete(name);
                    }
                })
            );
        }).then(() => self.clients.claim()) // 立即控制所有打开的页面
    );
});

// 3. 拦截请求:自定义缓存策略
self.addEventListener('fetch', (event) => {
    // 对HTML页面使用"网络优先,失败用缓存"策略
    if (event.request.headers.get('Accept').includes('text/html')) {
        event.respondWith(
            fetch(event.request)
                .then(networkResponse => {
                    // 更新缓存(将新的HTML存入缓存)
                    caches.open(CACHE_NAME).then(cache => {
                        cache.put(event.request, networkResponse.clone());
                    });
                    return networkResponse;
                })
                .catch(() => {
                    // 网络失败时,返回缓存的offline.html
                    return caches.match('/offline.html');
                })
        );
        return;
    }

    // 对其他资源(CSS、JS、图片)使用"缓存优先,没有再请求网络"策略
    event.respondWith(
        caches.match(event.request)
            .then(cacheResponse => {
                // 缓存存在则直接返回,同时后台更新缓存
                if (cacheResponse) {
                    // 后台请求网络并更新缓存
                    fetch(event.request).then(networkResponse => {
                        caches.open(CACHE_NAME).then(cache => {
                            cache.put(event.request, networkResponse.clone());
                        });
                    });
                    return cacheResponse;
                }

                // 缓存不存在,请求网络
                return fetch(event.request).catch(() => {
                    // 网络也失败(离线),返回默认占位图(如果是图片)
                    if (event.request.destination === 'image') {
                        return caches.match('/images/placeholder.webp');
                    }
                });
            })
    );
});

步骤3:更新缓存(修改Service Worker)

当静态资源更新时,只需:

  1. 修改CACHE_NAME(如my-app-cache-v2
  2. 更新ASSETS_TO_CACHE中的资源列表
  3. 浏览器会检测到sw.js内容变化,自动触发更新流程

4. Service Worker的适用场景

  • 离线可用的Web应用:如文档阅读器、新闻客户端(无网时显示缓存内容)。
  • 需要精准控制缓存的场景:如版本化缓存(不同版本资源隔离)、按用户角色缓存不同内容。
  • 优化重复访问体验:第二次访问时,所有资源从本地缓存读取,加载速度接近原生APP。

三、HTTP缓存与Service Worker:如何配合使用?

HTTP缓存和Service Worker不是替代关系,而是互补的:

  • 基础缓存用HTTP缓存:静态资源(图片、CSS、JS)优先通过Cache-Control设置长期强缓存,减少Service Worker的拦截压力。
  • 高级需求用Service Worker:离线访问、复杂缓存策略(如网络+缓存混合使用)、缓存更新控制等场景,用Service Worker实现。

配合示例

  1. 图片、第三方库通过HTTP缓存设置max-age=31536000,减少重复请求。
  2. HTML和业务JS通过Service Worker实现"网络优先+缓存兜底",确保更新及时且离线可用。
  3. API数据通过Service Worker的"网络优先"策略,同时缓存最新数据(下次离线时可用)。

总结:缓存是"性能优化的基石"

缓存机制通过减少重复请求,直接影响页面的加载速度(LCP)和用户体验(二次访问是否流畅)。

  • HTTP缓存是"默认配置",简单高效,适合大部分静态资源,只需通过服务器响应头即可控制。
  • Service Worker是"进阶工具",可编程、可定制,适合需要离线访问和复杂缓存策略的场景。

实际开发中,需根据资源类型和业务需求选择合适的缓存方案——记住:最好的缓存策略是" 用户感觉不到缓存的存在,但能感受到页面的极速体验"。