[TOC]
# 簡介

<br>
緩存可以說是性能優化中**簡單高效**的一種優化方式了,它可以**顯著減少網絡傳輸所帶來的損耗**。
對于一個數據請求來說,可以分為發起網絡請求、后端處理、瀏覽器響應三個步驟。瀏覽器緩存可以幫助我們在第一和第三步驟中優化性能。比如說直接使用緩存而不發起請求,或者發起了請求但后端存儲的數據和前端一致,那么就沒有必要再將數據回傳回來,這樣就減少了響應數據。
<br>
<br>
# 緩存位置
從緩存位置上來說分為四種,并且各自有**優先級**,當依次查找緩存且都沒有命中的時候,才會去請求網絡
1. Service Worker
2. Memory Cache
3. Disk Cache
4. Push Cache
5. 網絡請求
## Service Worker
Service Worker 是運行在瀏覽器背后的獨立線程,一般可以用來實現緩存功能。使用 Service Worker的話,傳輸協議必須為 HTTPS。因為 Service Worker 中涉及到請求攔截,所以必須使用 HTTPS 協議來保障安全。**Service Worker 的緩存與瀏覽器其他內建的緩存機制不同,它可以讓我們自由控制緩存哪些文件、如何匹配緩存、如何讀取緩存,并且緩存是持續性的**。
Service Worker 實現緩存功能一般分為三個步驟:首先需要先注冊 Service Worker,然后監聽到 install 事件以后就可以緩存需要的文件,那么在下次用戶訪問的時候就可以通過攔截請求的方式查詢是否存在緩存,存在緩存的話就可以直接讀取緩存文件,否則就去請求數據。
當 Service Worker 沒有命中緩存的時候,我們需要去調用 fetch 函數獲取數據。也就是說,如果我們沒有在 Service Worker 命中緩存的話,會根據緩存查找優先級去查找數據。但是不管我們是從 Memory Cache 中還是從網絡請求中獲取的數據,瀏覽器都會顯示我們是從 Service Worker 中獲取的內容。
<br>
## Memory Cache
Memory Cache 也就是內存中的緩存,讀取內存中的數據肯定比磁盤快。**但是內存緩存雖然讀取高效,可是緩存持續性很短,會隨著進程的釋放而釋放。** 一旦我們關閉 Tab 頁面,內存中的緩存也就被釋放了。
<br>
當我們訪問過頁面以后,再次刷新頁面,可以發現很多數據都來自于內存緩存

那么既然內存緩存這么高效,我們是不是能讓數據都存放在內存中呢?
<br>
先說結論,這是**不可能**的。首先計算機中的內存一定比硬盤容量小得多,操作系統需要精打細算內存的使用,所以能讓我們使用的內存必然不多。
<br>
內存緩存中有一塊重要的緩存資源是preloader相關指令(例如`<link rel="prefetch">`)下載的資源。總所周知preloader的相關指令已經是頁面優化的常見手段之一,它可以一邊解析js/css文件,一邊網絡請求下一個資源。
<br>
需要注意的事情是,**內存緩存在緩存資源時并不關心返回資源的HTTP緩存頭Cache-Control是什么值,同時資源的匹配也并非僅僅是對URL做匹配,還可能會對Content-Type,CORS等其他特征做校驗**。
<br>
## Disk Cache
Disk Cache 也就是存儲在硬盤中的緩存,讀取速度慢點,但是什么都能存儲到磁盤中,比之 Memory Cache**勝在容量和存儲時效性上。**
<br>
在所有瀏覽器緩存中,Disk Cache 覆蓋面基本是最大的。它會根據 HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。**并且即使在跨站點的情況下,相同地址的資源一旦被硬盤緩存下來,就不會再次去請求數據。**
<br>
## Push Cache
Push Cache 是 HTTP/2 中的內容,當以上三種緩存都沒有命中時,它才會被使用。**并且緩存時間也很短暫,只在會話(Session)中存在,一旦會話結束就被釋放。**
<br>
Push Cache 在國內能夠查到的資料很少,也是因為 HTTP/2 在國內不夠普及,但是 HTTP/2 將會是日后的一個趨勢。這里推薦閱讀[HTTP/2 push is tougher than I thought](https://link.juejin.im/?target=https%3A%2F%2Fjakearchibald.com%2F2017%2Fh2-push-tougher-than-i-thought%2F)這篇文章,但是內容是英文的,我翻譯一下文章中的幾個結論,有能力的同學還是推薦自己閱讀
* 所有的資源都能被推送,但是 Edge 和 Safari 瀏覽器兼容性不怎么好
* 可以推送`no-cache`和`no-store`的資源
* 一旦連接被關閉,Push Cache 就被釋放
* 多個頁面可以使用相同的 HTTP/2 連接,也就是說能使用同樣的緩存
* Push Cache 中的緩存只能被使用一次
* 瀏覽器可以拒絕接受已經存在的資源推送
* 你可以給其他域名推送資源
<br>
## 網絡請求
如果所有緩存都沒有命中的話,那么只能發起請求來獲取資源了。
<br>
那么為了性能上的考慮,大部分的接口都應該選擇好緩存策略,接下來我們就來學習緩存策略這部分的內容。
<br>
<br>
# 緩存策略
通常瀏覽器緩存策略分為兩種:**強緩存**和**協商緩存**,并且緩存策略都是通過設置 HTTP Header 來實現的。
<br>
## 強緩存
強緩存:不會向服務器發送請求,直接從緩存中讀取資源,在chrome控制臺的Network選項中可以看到該請求返回200的狀態碼,并且Size顯示from disk cache或from memory cache。強緩存可以通過設置兩種 HTTP Header 實現:Expires 和 Cache-Control。
<br>
### Expires
**緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點**。也就是說,Expires=max-age + 請求時間,需要和Last-modified結合使用。Expires是Web服務器響應消息頭字段,在響應http請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。
~~~
Expires: Wed, 22 Oct 2018 08:41:00 GMT
~~~
`Expires`是 HTTP/1 的產物,表示資源會在`Wed, 22 Oct 2018 08:41:00 GMT`后過期,需要再次請求。并且`Expires`**受限于本地時間**,如果修改了本地時間,可能會造成緩存失效。
### Cache-control
~~~
Cache-control: max-age=30
~~~
`Cache-Control`出現于 HTTP/1.1,**優先級高于`Expires`**。該屬性值表示資源會在 30 秒后過期,需要再次請求。
`Cache-Control`**可以在請求頭或者響應頭中設置**,并且可以組合使用多種指令

從圖中我們可以看到,我們可以將**多個指令配合起來一起使用**,達到多個目的。比如說我們希望資源能被緩存下來,并且是客戶端和代理服務器都能緩存,還能設置緩存失效時間等等。
<br>
Cache-Control 可以在請求頭或者響應頭中設置,并且可以組合使用多種指令:

<br>
* **public**:**所有內容都將被緩存(客戶端和代理服務器都可緩存)**。具體來說響應可被任何中間節點緩存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中間的proxy可以緩存資源,比如下次再請求同一資源proxy1直接把自己緩存的東西給 Browser 而不再向proxy2要。
* **private**:**所有內容只有客戶端可以緩存**,Cache-Control的默認取值。具體來說,表示中間節點不允許緩存,對于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 會老老實實把Server 返回的數據發送給proxy1,自己不緩存任何數據。當下次Browser再次請求時proxy會做好請求轉發而不是自作主張給自己緩存的數據。
* **no-cache**:客戶端緩存內容,是否使用緩存則需要經過協商緩存來驗證決定。表示不使用 Cache-Control的緩存控制方式做前置驗證,而是使用 Etag 或者Last-Modified字段來控制緩存。**需要注意的是,no-cache這個名字有一點誤導。設置了no-cache之后,并不是說瀏覽器就不再緩存數據,只是瀏覽器在使用緩存數據時,需要先確認一下數據是否還跟服務器保持一致。**
* **no-store**:所有內容都不會被緩存,即不使用強制緩存,也不使用協商緩存
* **max-age**:max-age=xxx (xxx is numeric)表示緩存內容將在xxx秒后失效
* **s-maxage**(單位為s):同max-age作用一樣,只在代理服務器中生效(比如CDN緩存)。比如當s-maxage=60時,在這60秒中,即使更新了CDN的內容,瀏覽器也不會進行請求。max-age用于普通緩存,而s-maxage用于代理緩存。**s-maxage的優先級高于max-age**。如果存在s-maxage,則會覆蓋掉max-age和Expires header。
* **max-stale**:能容忍的最大過期時間。max-stale指令標示了客戶端愿意接收一個已經過期了的響應。如果指定了max-stale的值,則最大容忍時間為對應的秒數。如果沒有指定,那么說明瀏覽器愿意接收任何age的響應(age表示響應由源站生成或確認的時間與當前時間的差值)。
* **min-fresh**:能夠容忍的最小新鮮度。min-fresh標示了客戶端不愿意接受新鮮度不多于當前的age加上min-fresh設定的時間之和的響應。
<br>
### Expires和Cache-Control兩者對比
其實這兩者差別不大,區別就在于 Expires 是http1.0的產物,Cache-Control是http1.1的產物,**兩者同時存在的話,Cache-Control優先級高于Expires**;在某些不支持HTTP1.1的環境下,Expires就會發揮用處。所以Expires其實是過時的產物,現階段它的存在只是一種兼容性的寫法。
強緩存判斷是否緩存的依據來自于是否超出某個時間或者某個時間段,而不關心服務器端文件是否已經更新,這可能會導致加載文件不是服務器端最新的內容,**那我們如何獲知服務器端內容是否已經發生了更新呢**?此時我們需要用到協商緩存策略。
<br>
## 協商緩存
**協商緩存就是強制緩存失效后,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程。**
當瀏覽器發起請求驗證資源時,如果資源沒有做改變,那么服務端就會返回 304 狀態碼,并且更新瀏覽器緩存有效期。

如果緩存過期了,就需要發起請求驗證資源是否有更新。協商緩存可以通過設置兩種 HTTP Header 實現:`Last-Modified`和`ETag`。
### Last-Modified 和 If-Modified-Since
`Last-Modified`表示本地文件最后修改日期,`If-Modified-Since`會將`Last-Modified`的值發送給服務器,詢問服務器在該日期后資源是否有更新,有更新的話就會將新的資源發送回來,否則返回 304 狀態碼。
但是`Last-Modified`存在一些弊端:
* 如果本地打開緩存文件,即使沒有對文件進行修改,但還是會造成`Last-Modified`被修改,服務端不能命中緩存導致發送相同的資源
* 因為`Last-Modified`只能以秒計時,如果在不可感知的時間內修改完成文件,那么服務端會認為資源還是命中了,不會返回正確的資源
因為以上這些弊端,所以在 HTTP / 1.1 出現了`ETag`。
### ETag 和 If-None-Match
`ETag`類似于文件指紋,`If-None-Match`會將當前`ETag`發送給服務器,詢問該資源`ETag`是否變動,有變動的話就將新的資源發送回來。并且`ETag`優先級比`Last-Modified`高。
以上就是緩存策略的所有內容了,看到這里,不知道你是否存在這樣一個疑問。**如果什么緩存策略都沒設置,那么瀏覽器會怎么處理?**
對于這種情況,瀏覽器會采用一個啟發式的算法,通常會取響應頭中的`Date`減去`Last-Modified`值的 10% 作為緩存時間。
<br>
<br>
# 緩存機制
**強制緩存優先于協商緩存進行,若強制緩存(Expires和Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match),協商緩存由服務器決定是否使用緩存,若協商緩存失效,那么代表該請求的緩存失效,返回200,重新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回304,繼續使用緩存**。具體流程圖如下:

<br>
**如果什么緩存策略都沒設置,那么瀏覽器會怎么處理?**
對于這種情況,瀏覽器會采用一個啟發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。
<br>
<br>
# 實際場景應用緩存策略
單純了解理論而不付諸于實踐是沒有意義的,接下來我們來通過幾個場景學習下如何使用這些理論。
## 頻繁變動的資源
對于頻繁變動的資源,首先需要使用`Cache-Control: no-cache`使瀏覽器每次都請求服務器,然后配合`ETag`或者`Last-Modified`來驗證資源是否有效。這樣的做法雖然不能節省請求數量,但是能顯著減少響應數據大小。
<br>
## 代碼文件
這里特指除了 HTML 外的代碼文件,因為 HTML 文件一般不緩存或者緩存時間很短。
一般來說,現在都會使用工具來打包代碼,那么我們就可以對文件名進行哈希處理,只有當代碼修改后才會生成新的文件名。基于此,我們就可以給代碼文件設置緩存有效期一年`Cache-Control: max-age=31536000`,這樣只有當 HTML 文件中引入的文件名發生了改變才會去下載最新的代碼文件,否則就一直使用緩存。
<br>
<br>
# 用戶操作行為與緩存
用戶在使用瀏覽器的時候,會有各種操作,比如輸入地址后回車,按F5刷新等,這些行為會對緩存有什么影響呢?

<br>
<br>
# 參考資料
[深入理解瀏覽器的緩存機制](https://www.jianshu.com/p/54cc04190252)
* 前端面試之道 - 掘金小冊
[no-cache,max-age=0,nostore區別及304原理](https://www.zhoulujun.cn/html/theory/network/2018_0306_8078.html)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼