浏览器缓存控制讲解

缓存的作用

在你访问互联网中的任何资源其所产生的任何链路中的每一个节点几乎都会进行缓存,整个缓存体系和细节十分复杂。比如浏览器缓存,服务器缓存,代理服务器缓存,CDN缓存等。

但是缓存又十分重要,不可缺少,为什么这么说呢?

由于HTTP请求的链路漫长,环境复杂,把一些必要的信息在关键的节点进行缓存,下次请求的时候可以尽可能的复用数据,就可以节省一部分资源的消耗,减少HTTP请求应答的成本,节约带宽,加快响应速度。

那么,基于请求-应答模式的特点,缓存大致可以分为服务器缓存和客户端缓存,而服务器缓存经常与代理服务关联在一起。这次我们主要关注浏览器是如何进行缓存的。

缓存的形式

Memory Cache(内存缓存)

  • 时间间隔较短时, 重复请求相同的静态资源时, 一般情况下资源还在内存中, 那直接从内存中获取资源, 这种方式是最快的资源获取方式。
  • 通常情况下, 关闭 tab 页签时, 会清除内存中的资源。

Disk Cache(硬盘缓存)

  • 把资源存储到本机的硬盘上, 当再次需要数据时, 可以直接从硬盘上获取。
  • 缓存的时间一般会根据响应头的设置而定。

CDN 缓存

  • 当用户请求资源时会经过 CDN 服务器, 服务器会对可以缓存的资源进行缓存。
  • 当有另外的用户也需要这个资源时, CDN 服务器就可以直接返回资源, 不需要再找原服务器要了。

服务端缓存

  • 当一些数据需要服务端进行大量计算才能获得时, 服务端为了避免重复的处理影响服务器性能, 开发人员会对处理过的数据进行缓存, 下次再需要时直接取缓存中的数据。
  • 服务端缓存一般是一些接口的缓存处理,缓存时效可能根据数据的性质不同而不同。

缓存控制

假设,现在没有缓存,我们想象一下获取资源的方式是什么样的?

客户端请求资源,服务器返回资源,等下一次想要获取同样资源的时候,哪怕服务器的资源并没有更新,还是要重新走一遍网络请求,然后服务器返回资源的完整链路。

那如果有了缓存,在客户端第一次获取到请求的资源后,把资源缓存到本地,下次再去请求的时候,发现本地有这个资源,直接拿来用就好了,完全不用去走网络请求。

HTTP缓存都是从第二次请求开始的,可以概述如下:

  • 第一次请求资源时:

    服务器返回资源,并在 response header(响应头) 中回传资源的缓存策略;

  • 第二次请求时:

    浏览器判断这些请求参数,击中强缓存就直接200

    否则就把请求参数加到request header(请求头)中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。

我们仔细的阅读一下这个简单的缓存资源请求流程,发现其中有几个重要的节点。

首先,服务器在返回该资源时,要标记该资源的有效期。然后,浏览器初次请求肯定是没缓存的,再次请求的时候,它要根据该资源的有效期来判断下一步该怎么办。

我们再简单一点,如果我们试图去获取缓存资源,其实是要看服务器的标记的。那么换句话说,服务器标记缓存资源,浏览器会验证该缓存资源的标记

浏览器每次发起请求时,会先在浏览器缓存中查找该请求的结果以及缓存标识,根据缓存标识来判断是否使用本地缓存。如果缓存有效,则使用本地缓存;否则,则向服务器发起请求并携带缓存标识。

根据是否需向服务器发起请求,将缓存过程划分为两个部分:强制缓存协商缓存,强缓存优先于协商缓存。

浏览器缓存控制机制有两种:HTML Meta 标签 和 HTTP 头信息

使用HTML Meta 标签

HTML Meta标签是应用在HTML文件中的head头部分。

<meta http-equiv="Cache-Control" content="max-age=7200" />
// or 
<meta http-equiv="Cache-Control" content="no-cache" />

主要作用就是告诉浏览器此HTML页面不被缓存,每次访问都去服务器上下载。

使用上很简单,但这个是 IE 时代的私有属性,在 IE9 以前支持的,而现在主流的 Chrome / Firefox / Safari,包括 IE9 ~ IE11 都不支持。而且所有缓存代理服务器都不支持。因为代理不解析HTML内容本身。

所以我们常说的浏览器缓存还是通过HTTP头信息来控制缓存。

http-equiv 倒确实在 HTML 规范中有几个值可以设( content-security-policycontent-type、default-style、x-ua-compatiblerefresh),但都跟缓存无关,可以参阅文档 http-equiv

使用HTTP头信息控制缓存

使用 HTTP 头信息进行缓存处理一般是通过设置相应的请求头/响应头实现的

常见的处理方式有 4 种

  • Cache-Control
  • Expires
  • Etag / If-None-Match
  • Last-Modified / If-Modified-Since

后3种是 http 1.0 就已经支持的,Cache-Control 是 http 1.1 支持的

缓存优先级: Cache-Control > Expires > Etag > Last-Modified

Cache-ControlExpires 首部用于指定缓存时间,Last-ModifiedETag 首部提供验证机制。

通过浏览器开发者工具我们可以看到,浏览器请求服务器静态资源的响应状态码主要就是下图的四种:

强缓存

通过 Expires(http1.0) 和 Cache-Control(http1.1) 两个响应头字段来控制,如果同时存在,则后者优先级高于前者。

服务器通知浏览器一个缓存时间,在缓存时间内,下次请求直接从本地缓存中读取资源而不发起请求。不在时间内,执行比较缓存策略。

  • 强缓存命中则直接读取浏览器本地的资源,在network中显示的是from memory cache或者from disk cache

  • Cache-Control是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。

  • Cache-Control的出现是为了解决 Expires 在浏览器时间被手动更改导致缓存判断错误的问题。

Expires 缓存过期时间

Expires是http响应头字段,是一个绝对时间,用以告诉浏览器这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求。

  • Expires 字段为一个日期, 客户端请求该资源时将这个日期与客户端当前日期进行比对。
  • 如果当前时间小于这个日期, 则表示资源未过期, 使用缓存。
  • 如果当前时间大于这个日期, 则表示资源已过期, 客户端就会重新请求该资源。

优势:

  • HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。
  • 以时刻标识失效时间,简易易理解

劣势:

  • 我们依赖客户端时间, 就会因为客户端的时间不准确导致判断错误,致使缓存失效。如果客户端时间早于服务器的时间, 会导致资源还未过期就重新请求。反之, 会导致客户端还在使用过期的旧资源。
Cache-Control 缓存控制

Cache-Control 通用消息头字段,被用于在 http 请求和响应中,通过指定指令来实现缓存机制。

缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中。

一般用在http响应中进行客户端缓存设置。

Cache-Control 是在 HTTP/1.1 中才有, 算是一个"新"的API (新是相对以前的处理, 因为现在已经有 HTTP3 了)

可选的参数有很多: max-age=no-storeno-cachemust-revalidateprivatepublic

在HTTP/1.1中增加的字段。属于相对时间。该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求。比Expires多了很多选项设置。

  1. max-age

    可以用来表示过期时长,单位是秒。表达式是这样子的:

    Cache-Control: max-age=30
    

    该资源的有效期是30秒。

    这是一个相对时间,时间计算的起点是报文创建的时刻,也就是响应头Date字段的时间,是指资源离开服务器的时刻,而不是客户端收到报文的时候。

    换句话说,假设我们设置的时间是5秒,但是链路请求很长,花费了4秒的时间,那么缓存对于浏览器的有效时间只是1秒。

    那你可能会说,就这一秒有啥用啊~~假设你网站的访问量特别大,每秒有上百万的访问,那你可以想象到这仅仅一秒的时间能节省服务器多大的压力了吧。当然,只是举个极限的例子~

    我们先搞个max-age的小例子,看看缓存能否生效:

    res.setHeader("Cache-Control", "max-age=20");
    

    我们可以看到在响应头中返回了我们设置好的缓存字段:

    除了max-age这个最常用的属性以外,还有三个属性可以更精确的指示浏览器如何使用缓存

  2. no-store

    浏览器和代理服务器禁止缓存,每次只能去服务器请求最新资源。用于某些变化非常频繁的页面,比如秒杀页面。

    Cache-Control: no-store
    
  3. no-cache

    它的字面意思和no-store很容易搞混,实际上它的意思并不是不允许缓存,而是可以缓存,只是不使用强缓存,每次使用前必须要去服务器验证是否过期,是否有最新的版本。同时,代理服务器也不能对资源进行缓存。一般用于长期不变的资源,客户端只需要发送很小的信息跟服务端进行确认 (协商缓存验证)。

  4. must-revalidate

    no-cache又很相似,它的意思是缓存不过期就可以继续使用,但是过期了如果还想用就必须去服务器验证一下。

  5. private

    只有浏览器可以缓存

  6. public

    浏览器,服务器,代理服务器都可以缓存

Cache-Control时指定 no-cachemax-age=0, must-revalidate 表示客户端可以缓存资源,但每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。

Cache-Control: no-cache
# or
Cache-Control: max-age=0, must-revalidate

优势:

  • HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。
  • Expires多了很多选项设置。

劣势:

  • 存在版本问题,到期之前的修改客户端是不可知的。
协商缓存

协商缓存有 2 组字段(不是两个),控制协商缓存的字段有:Last-Modified / If-Modified-Since(http1.0)和 Etag / If-None-Match(http1.1)。如果同时存在,则后者优先级高于前者。

Last-Modified / If-Modified-Since

Last-Modified很好理解,就是服务端返回最后一次修改文件的时间。

If-Modified-Since(客服端发起): 本地文件的修改时间(可以理解为上次请求返回的 Last-Modified的值),以后每次请求,请求头都会都带上。

如果强制缓存未命中,但协商缓存可用,需要第一次请求的时候,HTTP响应头中返回Last-ModifiedETag,当第二次请求的时候请求头会自动设置 If-Modified-Since 或者 If-None-Match 的值,服务端根据值,去决策验证是否命中协商缓存。

如果命中了协商缓存,那么服务器仅返回**304(不返回数据实体)**状态码,更新下资源的有效时间,使用本地缓存,因此在响应体体积上的节省是它的优化点。没有命中则返回200状态码。

优势:

  • 不存在版本问题,每次请求都会去服务器进行校验。

劣势:

  • 有新资源服务端却返回304,因为Last-Modified是以秒级为记录的,如果资源在1秒内改变的话,Last-Modified是无感的。
Etag / If-None-Match

ETag实体标签(Entity Tag) 的缩写。请求数据成功后, 服务端返回数据时响应头会携带 Etag,他是根据文件内容生成的一个字符串, 当文件变化时他也会跟着改变,一般都是 hash 生成的,是资源的唯一标识。

If-None-Match 是客户端发起的上次返回的 Etag 值。服务器会将 If-None-Match 的值与自己本地资源的 ETag 的值进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回最新数据。整个流程基本上和 (Last-Modified / If-Modified-Since) 很类似。

ETag主要是用来解决修改时间无法准确区分文件变化的问题。

比如,文件的修改时间是秒级甚至更短的,所以一秒内的新版本是无法区分的,再比如,一个文件定期更新,但有时内容没有变化,用修改时间就会以为发生了变化,发送给客户端以为是新的资源,浪费带宽。使用ETag就可以精确的识别资源的变动情况,让浏览器更有效地利用缓存。

ETag还有强弱之分。

ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个W/标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。至于是强还是弱,其实是由服务器自主决定的。弱也可以强,强也可以弱。

优势:

  • 可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
  • 不存在版本问题,每次请求都会去服务器进行校验。

劣势:

  • 计算ETag值需要性能损耗。
  • 分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时出现ETag不匹配的情况。

浏览器自身的缓存控制

客户端也可以控制缓存,客户端是怎么控制的呢?

当你点击浏览器的刷新按钮的时候,实际上,浏览器就在HTTP请求中夹带了Cache-Control:max-age=0,之前说过,max-age是生存时间,纪录的是从服务器生成的那一刻的有效期,而浏览器本地的资源,肯定不可能是0,所以当浏览器加上了max-age=0的时候,每次都会向服务器请求最新的资源。

而当你使用Control+F5强制刷新的时候其实是浏览器发了一个Cache-Control: no-cache。基本上和max-age=0差不多一样的效果,但是含义肯定是不一样的,就看服务器要怎么理解和处理不同的字段。

那么什么时候缓存才能派上用场呢?当我们点击浏览器的前进后退按钮的时候,就会直接从缓存中获取数据,另外,重定向的时候,也可能会使用到缓存。那这两类操作有啥区别呢。其实本质上来说,就是前进、后退、跳转这样的操作,浏览器不会私自加上Cache-Control字段,并且会清空If-Modified-SinceIf-None-Match字段,所以就会检查缓存,直接利用之前的资源,不再进行网络通信。

大家还可以自己尝试设置no-storeno-cache等字段。当你设置了no-store属性后,你会发现,哪怕使用浏览器的前进,后退按钮,每次也是重新从服务器获取资源,但是no-cachemax-age则会使用缓存。

哪些请求不会被缓存?

  • HTTP 信息头中包含 Cache-Control:no-cache,pragma:no-cache,或 Cache-Control:max-age=0 等告诉浏览器不用缓存的请求。

  • HTTP响应头中不包含Cache-Control/Expires,也不包含Last-Modified/ETag的请求无法被缓存。

  • POST 请求无法被缓存

部署时缓存的问题

我们不仅要缓存代码,还需要更新代码。如果静态资源名字不变,怎么让浏览器既能缓存,又能在有新代码时更新?

最简单的解决方式就是静态资源路径添加一个版本值。

版本不变就走缓存策略,版本变了就加载新资源。如下:

<script src="xx/xx.js?v=24334452"></script>

然而这种处理方式在部署时有问题。

背景:静态资源和页面是分开部署的

无论是先部署页面,还是先部署静态资源,在特定的阶段访问,就会出现页面和js文件不匹配进而报错。

这些问题的本质是以上的部署方式是“覆盖式发布”,解决方式是“非覆盖式发布”。

即用静态资源的文件摘要信息给文件命名,这样每次更新资源不会覆盖原来的资源,先将资源发布上去。

这时候存在两种资源,用户用旧页面访问旧资源,然后再更新页面,用户变成新页面访问新资源,就能做到无缝切换。

简单来说就是给静态文件名加hash值。

那如何实现呢?使用webpack持久化缓存

现在前端代码都用webpack之类的构建工具打包。浏览器有其缓存机制,想要既能缓存又能在部署时没有问题,需要给静态文件名添加hash值。

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
前端性能优化实践# 知识体系与小册格局 ## 写给读者 提起性能优化,大家现在脑海里第一时间会映射出什么内容呢? 可能是类似[“雅虎军规”](https://developer.yahoo.com/performance/rules.html?guccounter=1)和[《高性能 JavaScript》](https://book.douban.com/subject/5362856/)这样历久弥香的经典之作,也可能是搜索引擎聚合给你的一篇又一篇以性能优化为主题的个人或团队实践而来的“私货”。至少当我确定自己的研发方向、并接到第一个性能优化任务时,我做的第一件事是向搜索引擎求助,第二件事是买书,然后开始了摸着石头过河,前后花费了大量的时间和精力。我深感性能优化实在是前端知识树中特别的一环——当你需要学习前端框架时,文档和源码几乎可以告诉你所有问题的答案,当你需要学习 Git 时,你也可以找到放之四海皆准的实践方案。但性能优化却不一样,它好像只能是一个摸索的过程。 这个摸索的过程是痛苦的、漫长的,也是紧要的。因为在如今的互联网环境下,一个前端团队如果只把性能优化这个任务写在纸上,而不投入实践,它将缺失最基本的竞争力。 笔者写这本小册,是希望通过短短十数个章节的讲解,尽可能降低一些大家学习性能优化的成本。 一方面,这本小册为没有接触过性能优化的新同学建立起一个正确的前端性能优化的“世界观”,知道性能优化是什么、为什么、怎么做,从而使性能优化这件事情有迹可循,有路可走。这样在面试现场被问到性能优化层面的问题时,能够做到滔滔不绝、言之有物,而非像背书一样罗列干巴巴的知识点,最终淹没在茫茫的求职大军中。另一方面,小册可以为在职的工程师们提供一线团队已经实践过的“方法论”,知道什么场景下该做什么事情,最终在脑海中留下一张涵盖核心原理和实践的、可随时查阅并且高度可扩展的性能优化思路索引表。然后在今后的开发生活中可以去践行它,更进一步去挖掘它。把性能优化变作你前端工程师生涯的一门必修课,进而演化为自己研发方面的核心竞争力。 同时,相信大家可以明确这样一个学习观念:任何技术的掌握,都离不开一定比例的理论基础和实际操作的支撑。 具体到前端性能优化这件事情上,我认为它是 20% 的理论,加上至少 80% 的实践,甚至很多理论本身也都是我们在具体的业务场景中实践出来的。所以希望大家阅读本小册时,能够读到一些“书本之外的东西”——最好是一边读一边回忆自己既有的开发经历,尝试去留意哪些知识是已知的,哪些是未知的。 这样读完之后,就可以有的放矢地把这些知识转换为自己的项目实践——前端技术日新月异,性能方案永远都在更迭,所以一定要形成自己的学习思路。 建议每一位读者都带着“学了就要用”的心态去读这本小册。如果阅读结束,能够为你带来哪怕一个小小的开发习惯或者优化观念上的改变,这数小时的阅读时间就算没有白费。 ## 知识体系: 从一道面试题说起 在展开性能优化的话题之前,我想先抛出一个老生常谈的面试问题: > 从输入 URL 到页面加载完成,发生了什么? 这个问题非常重要,因为我们后续的内容都将以这个问题的答案为骨架展开。我希望正在阅读这本小册的各位可以在心里琢磨一下这个问题——无须你调动太多计算机的专业知识,只需要你用最快的速度在脑海中架构起这个抽象的过程——我们接下来所有的工作,就是围绕这个过程来做文章。 我们现在站在性能优化的角度,一起简单地复习一遍这个经典的过程:首先我们需要通过 DNS(域名解析系统)将 URL 解析为对应的 IP 地址,然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操作(如下图所示)。 ![](https://user-gold-cdn.xitu.io/2018/10/18/16685737b823244c?w=489&h=329&f=png&s=19023) 我们将这个过程切分为如下的过程片段: 1. DNS 解析 2. TCP 连接 3. HTTP 请求抛出 4. 服务端处理请求,HTTP 响应返回 5. 浏览器拿到响应数据,解析响应内容,把解析的结果展示给用户 大家谨记,我们任何一个用户端的产品,都需要把这 5 个过程滴水不漏地考虑到自己的性能优化方案内、反复权衡,从而打磨出用户满意的速度。 ## 从原理到实践:各个击破 我们接下来要做的事情,就是针对这五个过程进行分解,各个提问,各个击破。 具体来说,DNS 解析花时间,能不能尽量减少解析次数

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

定栓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值