浏览器的存储策略
1 本地存储与会话存储
sessionStorage本地存储和localStorage会话存储是Web浏览器提供的两种用于在客户端存储数据的机制,它们都遵循同源策略(协议、域名、端口相同),以键值对(key: value)的形式存储数据,且存储的值==只能是字符串类型==(若存储其他类型,会自动转换为字符串,读取时需手动转换回原类型) 。
但在存储有效期、数据共享范围等方面存在差异:
| 特性 | sessionStorage | localStorage |
|---|---|---|
| 有效期 | 仅在当前会话(浏览器标签页或窗口打开期间)有效,==关闭标签页或窗口后==,存储的数据会被自动清除 | 除非主动调用代码删除或用户手动清除浏览器缓存,否则数据会==一直保留在本地== |
| 数据共享范围 | ==不同标签页或窗口==(即使是同一页面的不同实例)之间==无法共享==数据,只有在同一个标签页内通过window.open打开的新页面才能访问到相同的sessionStorage数据 | ==同一域名下==的所有页面都可以访问和修改localStorage中的数据,不同标签页、窗口之间数据是共享的 |
| 应用场景 | 适合临时存储一些仅在当前会话中使用的数据,比如多步骤表单中每一步的临时数据、当前会话的用户浏览记录等 | 适用于需要长期保存的用户偏好设置(如主题设置、字体大小等)、用户登录状态的持久化存储等 |
常用方法
sessionStorage和localStorage拥有相似的方法:
- 存储数据(
setItem):使用setItem方法可以向sessionStorage或localStorage中添加键值对。
// 向 sessionStorage 中存储数据
sessionStorage.setItem('username', 'John');
// 向 localStorage 中存储数据
localStorage.setItem('theme', 'dark');- 读取数据(
getItem):通过getItem方法获取存储的数据,如果指定的键不存在,则返回null。
const username = sessionStorage.getItem('username');
const theme = localStorage.getItem('theme');- 删除数据(
removeItem):使用removeItem方法可以删除指定键的数据。
sessionStorage.removeItem('username');
localStorage.removeItem('theme');- 清除所有数据(
clear):clear方法会一次性删除sessionStorage或localStorage中存储的所有数据。
sessionStorage.clear();
localStorage.clear();注意点
- 数据类型转换:由于
sessionStorage和localStorage==只能存储字符串==,当存储对象、数组等复杂数据类型时,==需要使用JSON.stringify将其转换为字符串==,读取时再使用JSON.parse转换回原数据类型。
const user = { name: 'Alice', age: 30 };
// 存储对象
localStorage.setItem('user', JSON.stringify(user));
// 读取对象
const retrievedUser = JSON.parse(localStorage.getItem('user'));- 存储容量限制:不同浏览器对
sessionStorage和localStorage的存储容量限制有所不同,一般来说,==大约在5 - 10MB之间==。如果存储的数据超过了限制,会抛出QuotaExceededError异常 。 - 安全性:它们存储在客户端,数据相对容易被获取,==不适合存储敏感信息==(如密码等)。同时,在使用
localStorage时,要注意防止跨站点脚本攻击(XSS),因为攻击者可能会利用漏洞读取或修改localStorage中的数据。
2 Cookie
用于在前端对后端发送请求时会带上cookie中的内容,但其存储在本地且可以直接访问查看的特性,使其不能存储用户的私密信息与账密
属性详解
// Cookie 格式:key=value; attributes
document.cookie = "username=john; domain=.example.com; path=/; max-age=3600; secure; samesite=strict";[补充说明]:Cookie 主要属性包括:
- key=value:键值对([原笔记:fatty,修正为键值对])
- domain:作用域名
- 设置为
.a.com与a.com可作用于 a.com 及其所有子域名 - 不设置和设置则仅作用于当前域名
- 设置为
- path:URL路径限制(较少使用)
- max-age:存活时间(秒),优先级高于expires
- 正数:存活时间
- 0:立即删除
- 负数:会话结束时删除
- expires:绝对过期时间(GMT格式)
- secure:仅通过HTTPS传输
- httponly:禁止JavaScript访问(增强安全性)
- samesite:跨站请求限制
Strict:严格模式,完全禁止跨站发送Lax:宽松模式,允许部分安全请求跨站发送None:无限制(必须与secure同时使用)
CORS 跨域资源共享
[补充说明]:跨域请求处理机制:
- 简单请求:直接发送,包含Origin头部
- 非简单请求:先发送OPTIONS预检请求
服务器响应头部:
Access-Control-Allow-Origin: https://example.com # 允许的源
Access-Control-Allow-Methods: GET, POST, PUT # 允许的方法
Access-Control-Allow-Credentials: true # 是否允许携带凭证
Access-Control-Max-Age: 86400 # 预检请求缓存时间3 Session
由于http是无状态的,使用Session ID来保存用户的信息。通过将用户的信息存储在服务器中,再生成一个session用来代表用户的登录状态:
在收到服务器的相应之后,前端每次访问都会附带cookie中的内容
4 Token
JWT(JSON Web Token)用来解决Session存储在服务器以及在多个服务器中登录转态可能不一致的问题,Token是存储于用户端的: 
5 浏览器缓存实现
由于HTTP仅仅规定了缓存,但是没有规定如何实现,由浏览器自行实现 在强缓存的过程中,浏览器会将缓存的数据存储在不同的位置:
Memory Cache
Memory Cache 翻译过来便是“内存缓存”,顾名思义,它是存储在浏览器内存中的。其优点为获取速度快、优先级高,从内存中获取资源耗时为 0 ms,而其缺点也显而易见,比如生命周期短,当网页关闭后内存就会释放,同时虽然内存非常高效,但它也受限制于计算机内存的大小,是有限的。 
Disk Cache
那么如果要存储大量的资源,这是还得用到磁盘缓存。
Disk Cache 翻译过来是“磁盘缓存”的意思,它是存储在计算机硬盘中的一种缓存,它的优缺点与 Memory Cache 正好相反,比如优点是生命周期长,不触发删除操作则一直存在,而缺点则是获取资源的速度相对内存缓存较慢。
Disk Cache 会根据保存下来的资源的 HTTP 首部字段来判断它们是否需要重新请求,如果重新请求那便是强缓存的失效流程,否则便是生效流程。
从两者的优缺点中我们可以发现,Memory Cache 与 Disk Cache 珠联璧合,优势互补,共同构成了浏览器本地缓存的左右手。
缓存获取顺序
因为目前市面上浏览器众多,不同浏览器的缓存机制都可能不同,还是以主流的 Chrome 为例进行介绍。
浏览器缓存机制包含了 Http 缓存中强缓存、协商缓存的知识点,这里就不再进行赘述,下面主要介绍与 Memory Cache、 Disk Cache 相关的机制。
按照缓存顺序来讲,当一个资源准备加载时,浏览器会根据其三级缓存原理进行判断。
- 浏览器会率先查找内存缓存,如果资源在内存中存在,那么直接从内存中加载
- 如果内存中没找到,接下去会去磁盘中查找,找到便从磁盘中获取
- 如果磁盘中也没有找到,那么就进行网络请求,并将请求后符合条件的资源存入内存和磁盘中
按照以上顺序,浏览器缓存与 HTTP 缓存才得以相辅相成,在有效的沟通和判断中尽可能的减少不必要的资源浪费。
缓存存储优先级
上述我们讲解了缓存资源的获取顺序,那么在获取之前,浏览器又是按照什么优先级来存储资源的?这一问题也可以直接换成“浏览器判断一个资源是存入内存缓存还是磁盘缓存的依据是什么?”。
其实答案在介绍内存缓存和磁盘缓存时已经有所涉及,我们以掘金首页为例子进行介绍。
当我们打开开发者工具并在浏览器输入 url 访问后,发现除了 base64 的图片永远从内存加载外,其他大部分资源会从磁盘加载。
磁盘缓存会将命中强缓存的 js、css、图片等资源都收入囊中,也省去我们担心它“挑食”的问题。
而内存缓存不这样,为了保持“苗条的身材”,它不得不控制“饮食”,尽可能的去挑选适合自己的“食物”。此时我们刷新下页面让内存缓存生效:
我们先过滤下只看 JS 资源的加载情况,发现有些被内存缓存了,有些则没有,这是为什么?
有些读者可能会猜测是不是没有被缓存的是因为资源比较大,其实不然,上方图片笔者圈出了 Initiator 列,通过该列便可以找到答案。
Initiator 列表示资源加载发起的位置,我们点击从内存获取资源的该列值后可以发现资源是在 HTML 渲染阶段就被加载的,如以下代码所示:
<!DOCTYPE html> <html lang="zh-CN"> <head> <title>Demo</title> <script src="https://i.snssdk.com/slardar/sdk.js"></script> </head> <body> <div id="cache">加载的 JS 资源大概率会存储到内存中</div> </body> </html>
而被内存抛弃的资源我们也可以发现其都是异步加载的资源,这些资源没有被内存缓存,比如像这样:
<!DOCTYPE html> <html lang="zh-CN"> <head> <title>Demo</title> </head> <body> <div id="cache">异步加载的 JS 资源没有存储到内存中</div> <script> window.onload = function () { setTimeout(function () { var s = document.createElement("script"); s.type = "text/javascript"; s.async = true; s.src = "https://i.snssdk.com/slardar/sdk.js"; var x = document.getElementsByTagName("script")[0]; x.parentNode.insertBefore(s, x); }, 2000); }; </script> </body> </html>
根据以上测试代码很容易产生错误的判断结论:异步加载的 JS 资源不会存储到内存中。
> 浏览器内存缓存生效的前提下,JS 资源的执行加载时间会影响其是否被内存缓存
我们可以修改上述的 setTimeout 时间为 1 秒后再次进行验证,大家会发现即使异步了,JS 资源还是很容易被内存缓存,原因便是异步 JS 资源加载时浏览器渲染进程可能还没有结束,而进程没结束就有被存入内存的可能。
此外图片资源(非 base64)也有和 JS 资源同样的现象,而 CSS 资源比较与众不同,其被磁盘缓存的概率远大于被内存缓存。
这一现象目前还没有找到标准的答案,网上给出的非标准解释是:
> 因为 CSS 文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是 JS 之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样 IO 开销就很大了,有可能导致浏览器失去响应。
以上所述的内存缓存(Memory Cache)在浏览器标准中并没有详尽的描述,笔者也是根据自身实践及总结得出的一些结论,不同的浏览器在加载资源时可能会所有差异,读者还需根据自己的理解和实践进行进一步探索。
Preload 与 Prefetch
基于上述现象的前提下,笔者还发现了与资源加载相关的两个功能(Preload 与 Prefetch)也会潜移默化的影响着浏览器缓存。
preload 也被称为预加载,其用于 link 标签中,可以指明哪些资源是在页面加载完成后即刻需要的,浏览器会在主渲染机制介入前预先加载这些资源,并不阻塞页面的初步渲染。例如:
<link rel="preload" href="https://i.snssdk.com/slardar/sdk.js" as="script" />
而当使用 preload 预加载资源后,笔者发现该资源一直会从磁盘缓存中读取,JS、CSS 及图片资源都有同样的表现,这主要还是和资源的渲染时机有关,在渲染机制还没有介入前的资源加载不会被内存缓存。
相反 prefetch 则表示预提取,告诉浏览器加载下一页面可能会用到的资源,浏览器会利用空闲状态进行下载并将资源存储到缓存中。
<link rel="prefetch" href="https://i.snssdk.com/slardar/sdk.js" />
使用 prefetch 加载的资源,刷新页面时大概率会从磁盘缓存中读取,如果跳转到使用它的页面,则直接会从磁盘中加载该资源。
利用好 preload 和 prefetch 这“两员大将”,我们可以优化浏览器资源加载的顺序和时机,在页面性能优化环节至关重要。