為什么要使用web緩存?
Web緩存存在於服務器和客戶端之間。Web緩存密切注視着服務器-客戶端之間的通信,監控請求,並且把請求輸出的內容(例如html頁面、 圖片和文件)另存一份;然后,如果下一個請求是相同的URL,則直接使用保存的副本,而不是再次請求源服務器。
使用Web緩存的好處是顯而易見的:
-
減少網絡延遲,加快頁面打開速度--緩存比源服務器離客戶端更近,因此,從緩存請求內容比從源服務器所用時間更少,緩存的使用能夠明顯加快頁面打開速度,達到更好的體驗。
- 降低服務器的壓力--給網絡資源設定有效期之后,用戶可以重復使用本地的緩存,減少對源服務器的請求,間接降低服務器的壓力。同時,搜索引擎的爬蟲機器人也能根據過期機制降低爬取的頻率,也能有效降低服務器的壓力。
- 減少網絡帶寬損耗--無論對於網站運營者或者用戶,帶寬都代表着金錢,當Web緩存副本被使用時,只會產生極小的網絡流量,可以有效的降低運營成本。
現在的大型網站,隨便一個頁面都是一兩百個請求,每天 pv 都是億級別,如果沒有緩存,用戶體驗會急劇下降、同時服務器壓力和網絡帶寬都面臨嚴重的考驗。 緩存和重用以前獲取的資源的是優化網頁性能很重要的一個方面。
缺點也是有的:
- 緩存沒有清理機制--這些緩存的文件會永久性地保存在機器上,在特定的時間內,這些文件可能是幫了你大忙,但是時間一長,我們已經不再需要瀏覽之前的這些網頁,這些文件就成了無效或者無用的文件,它們存儲在用戶硬盤中只會占用空間而沒有任何用處,如果要緩存的東西非常多,那就會撐暴整個硬盤空間。
- 給開發帶來的困擾--明明修改了樣式文件、圖片、視頻或腳本,刷新頁面或部署到站點之后看不到修改之后的效果。
所以在產品開發的時候我們總是想辦法避免緩存產生,而在產品發布之時又在想策略管理緩存提升網頁的訪問速度。了解瀏覽器的緩存命中原理和清除方法,對我們大有裨益。
緩存的分類
在Web應用領域,Web緩存大致可以分為以下幾種類型:
1.數據庫數據緩存
Web應用,特別是社交網絡服務類型的應用,往往關系比較復雜,數據庫表繁多,如果頻繁進行數據庫查詢,很容易導致數據庫不堪重荷。為了提供查詢的性能,會將查詢后的數據放到內存中進行緩存,下次查詢時,直接從內存緩存直接返回,提供響應效率。比如常用的緩存方案有memcached,redis等。
2.服務器端緩存
代理服務器緩存
代理服務器是瀏覽器和源服務器之間的中間服務器,瀏覽器先向這個中間服務器發起Web請求,經過處理后(比如權限驗證,緩存匹配等),再將請求轉發到源服務器。代理服務器緩存的運作原理跟瀏覽器的運作原理差不多,只是規模更大。可以把它理解為一個共享緩存,不只為一個用戶服務,一般為大量用戶提供服務,因此在減少相應時間和帶寬使用方面很有效,同一個副本會被重用多次。常見代理服務器緩存解決方案有Squid,Nginx,Apache等。
CDN緩存
CDN(Content delivery networks)緩存,也叫網關緩存、反向代理緩存。CDN緩存一般是由網站管理員自己部署,為了讓他們的網站更容易擴展並獲得更好的性能。瀏覽器先向CDN網關發起Web請求,網關服務器后面對應着一台或多台負載均衡源服務器,會根據它們的負載請求,動態將請求轉發到合適的源服務器上。雖然這種架構負載均衡源服務器之間的緩存沒法共享,但卻擁有更好的處擴展性。從瀏覽器角度來看,整個CDN就是一個源服務器,瀏覽器和服務器之間的緩存機制,在這種架構下同樣適用。
3.瀏覽器端緩存
瀏覽器緩存根據一套與服務器約定的規則進行工作,在同一個會話過程中會檢查一次並確定緩存的副本足夠新。如果你瀏覽過程中,比如前進或后退,訪問到同一個圖片,這些圖片可以從瀏覽器緩存中調出而即時顯現。
4.Web應用層緩存
應用層緩存指的是從代碼層面上,通過代碼邏輯和緩存策略,實現對數據,頁面,圖片等資源的緩存,可以根據實際情況選擇將數據存在文件系統或者內存中,減少數據庫查詢或者讀寫瓶頸,提高響應效率。
緩存如何發揮作用
請看下面的圖:
緩存控制設置字段和原理
1.HTML Meta標簽控制緩存
瀏覽器緩存機制,其實主要就是HTTP協議定義的緩存機制(如: Expires; Cache-control等)。但是也有非HTTP協議定義的緩存機制,如使用HTML Meta 標簽,Web開發者可以在HTML頁面的<head>節點中加入<meta>標簽,代碼如下:
<META HTTP-EQUIV="Pragma" CONTENT="no-cache"> <META HTTP-EQUIV="Expires" CONTENT="0">
上述代碼的作用是告訴瀏覽器當前頁面不被緩存,每次訪問都需要去服務器拉取。使用上很簡單,但只有部分瀏覽器可以支持,
事實上這種禁用緩存的形式用處很有限:
1. 僅有IE才能識別這段meta標簽含義,其它主流瀏覽器僅能識別“Cache-Control: no-store”的meta標簽。
2. 在IE中識別到該meta標簽含義,並不一定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)。
而且所有緩存代理服務器都不支持,因為代理不解析HTML內容本身。而廣泛應用的還是 HTTP頭信息來控制緩存,下面我主要介紹HTTP協議定義的緩存機制
2.HTTP頭信息控制緩存
我們先來瞅一眼http1.1協議報文首部字段中與緩存相關的字段
1.通用首部字段
2.請求首部字段
3.響應首部字段
4.實體首部字段
http1.0 時代緩存字段詳解
在 http1.0 時代,給客戶端設定緩存方式可通過兩個字段Pragma
和Expires
來規范。雖然這兩個字段早可拋棄,但http協議做了向下兼容,所以依然可以看到。
1.Pragma
Pragma:設置頁面是否緩存,為Pragma則緩存,no-cache則不緩存
當該字段值為no-cache
的時候,會知會客戶端不要對該資源讀緩存,即每次都得向服務器發一次請求才行。
2.Expires
有了Pragma來禁用緩存,自然也需要有個東西來啟用緩存和定義緩存時間,對http1.0而言,Expires就是做這件事的首部字段。 Expires的值對應一個GMT(格林尼治時間),比如Mon, 22 Jul 2002 11:12:01 GMT
來告訴瀏覽器資源緩存過期時間,如果還沒過該時間點則不發請求。
如果Pragma頭部和Expires頭部同時存在,則起作用的會是Pragma,需要注意的是,響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,其定義的是資源“失效時刻”,如果客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了自己電腦的系統時間),那緩存時間可能就沒啥意義了。
http1.1時代緩存字段詳解
1.Cache-Control
針對上述的“Expires時間是相對服務器而言,無法保證和客戶端時間統一”的問題,http1.1新增了 Cache-Control 來定義緩存過期時間。注意:若報文中同時出現了 Expires 和 Cache-Control,則以 Cache-Control 為准。
也就是說優先級從高到低分別是 Pragma -> Cache-Control -> Expires 。
Cache-Control也是一個通用首部字段,這意味着它能分別在請求報文和響應報文中使用。在RFC中規范了 Cache-Control 的格式為:
"Cache-Control" ":" cache-directive
作為請求首部時,cache-directive 的可選值有:
Cache-Control: no-cache:這個很容易讓人產生誤解,使人誤以為是響應不被緩存。實際上Cache-Control: no-cache是會被緩存的,
只不過每次在向客戶端(瀏覽器)提供響應數據時,緩存都要向服務器評估緩存響應的有效性。
Cache-Control: no-store:這個才是響應不被緩存的意思。
作為響應首部時,cache-directive 的可選值有:
Cache-Control 允許自由組合可選值,例如:
Cache-Control: max-age=3600, must-revalidate
它意味着該資源是從原服務器上取得的,且其緩存(新鮮度)的有效時間為一小時,在后續一小時內,用戶重新訪問該資源則無須發送請求。
當然這種組合的方式也會有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。
2.Last-Modified/If-Modified-Since
Last-Modified/If-Modified-Since要配合Cache-Control使用。
(1) Last-Modified:標示這個響應資源的最后修改時間。web服務器在響應請求時,告訴瀏覽器資源的最后修改時間。
(2) If-Modified-Since:當資源過期時(使用Cache-Control標識的max-age),發現資源具有Last-Modified聲明,則再次向web服務器請求時帶上頭 If-Modified-Since,表示請求時間。web服務器收到請求后發現有頭If-Modified-Since 則與被請求資源的最后修改時間進行比對。若最后修改時間較新,說明資源又被改動過,則響應整片資源內容(寫在響應消息包體內),HTTP 200;若最后修改時間較舊,說明資源無新修改,則響應HTTP 304 (無需包體,節省瀏覽),告知瀏覽器繼續使用所保存的cache。
3.Etag/If-None-Match
Etag/If-None-Match也要配合Cache-Control使用。
(1) Etag:web服務器響應請求時,告訴瀏覽器當前資源在服務器的唯一標識(生成規則由服務器覺得)。Apache中,ETag的值,默認是對文件的索引節(INode),大小(Size)和最后修改時間(MTime)進行Hash后得到的。
(2)If-None-Match:當資源過期時(使用Cache-Control標識的max-age),發現資源具有Etage聲明,則再次向web服務器請求時帶上頭If-None-Match (Etag的值)。web服務器收到請求后發現有頭If-None-Match 則與被請求資源的相應校驗串進行比對,決定返回200或304。
4.既生Last-Modified何生Etag?
你可能會覺得使用Last-Modified已經足以讓瀏覽器知道本地的緩存副本是否足夠新,為什么還需要Etag(實體標識)呢?HTTP1.1中Etag的出現主要是為了解決幾個Last-Modified比較難解決的問題:
(1) Last-Modified標注的最后修改只能精確到秒級,如果某些文件在1秒鍾以內,被修改多次的話,它將不能准確標注文件的修改時間
(2)如果某些文件會被定期生成,當有時內容並沒有任何變化,但Last-Modified卻改變了,導致文件沒法使用緩存
(3)有可能存在服務器沒有准確獲取文件修改時間,或者與代理服務器時間不一致等情形
Etag是服務器自動生成或者由開發者生成的對應資源在服務器端的唯一標識符,能夠更加准確的控制緩存。Last-Modified與ETag是可以一起使用的,服務器會優先驗證ETag,一致的情況下,才會繼續比對Last-Modified,最后才決定是否返回304。
5.不太常用的兩個http字段If-Unmodified-Since/If-Match
(1)If-Unmodified-Since: Last-Modified-value
告訴服務器,若Last-Modified沒有匹配上(資源在服務端的最后更新時間改變了),則應當返回412(Precondition Failed) 狀態碼給客戶端。
當遇到下面情況時,If-Unmodified-Since 字段會被忽略:
1. Last-Modified值對上了(資源在服務端沒有新的修改); 2. 服務端需返回2XX和412之外的狀態碼; 3. 傳來的指定日期不合法
(2)If-Match: ETag-value
告訴服務器如果沒有匹配到ETag,或者收到了“*”值而當前並沒有該資源實體,則應當返回412(Precondition Failed) 狀態碼給客戶端。否則服務器直接忽略該字段。
瀏覽器緩存流程圖
小結一下,瀏覽器第一次請求
瀏覽器第二次請求
如何配置
1)通過代碼的方式,在web服務器返回的響應中添加Expires和Cache-Control Header;
比如在JavaWeb里面,我們可以使用類似下面的代碼設置強緩存:
java.util.Date date = new java.util.Date(); response.setDateHeader("Expires",date.getTime()+20000); //Expires:過時期限值 response.setHeader("Cache-Control", "public"); //Cache-Control來控制頁面的緩存與否,public:瀏覽器和緩存服務器都可以緩存頁面信息; response.setHeader("Pragma", "Pragma"); //Pragma:設置頁面是否緩存,為Pragma則緩存,no-cache則不緩存
還可以通過類似下面的java代碼設置不啟用強緩存:
response.setHeader( "Pragma", "no-cache" );
response.setDateHeader("Expires", 0); response.addHeader( "Cache-Control", "no-cache" );//瀏覽器和緩存服務器都不應該緩存頁面信息
2)通過配置web服務器的方式,讓web服務器在響應資源的時候統一添加Expires和Cache-Control Header。
tomcat提供了一個ExpiresFilter專門用來配置強緩存
nginx和apache作為專業的web服務器,都有專門的配置文件,可以配置expires和cache-control,
Nginx服務器的配置方法為:
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { #過期時間為30天,#圖片文件不怎么更新,過期可以設大一點, expires 30d; } location ~ .*\.(js|css)$ { #如果頻繁更新,則可以設置得小一點。 expires 1d;
add_header Cache-Control max-age=86400;
etag on;
}
Apache服務器的配置方法為:
<Location ~ "\.(js|css|png|jpg|gif|bmp|html)$"> ExpiresActive On ExpiresDefault "access plus 1 hours" Header set Cache-Control max-age=3600 Header unset Pragma </Location> <Location ~ "\.(do|jsp|aspx|asp|php|json|action|ashx|axd|cgi)$"> Header set Cache-Control no-cache,no-store,max-age=0 Header unset Expires
Etag INode Mtime Size </Location>
3.緩存配置的一些注意事項
1.只有get請求會被緩存,post請求不會
2.Etag 在資源分布在多台機器上時,對於同一個資源,不同服務器生成的Etag可能不相同,此時就會導致304協議緩存失效,客戶端還是直接從server取資源。可以自己修改服務器端etag的生成方式,根據資源內容生成同樣的etag。需要注意的是分布式系統里多台機器間文件的last-modified必須保持一致,以免負載均衡到不同機器導致比對失敗,Yahoo建議分布式系統盡量關閉掉Etag(每台機器生成的etag都會不一樣,因為除了 last-modified、文檔節點也很難保持一致)
用戶行為與緩存
緩存的清除方法
由於在開發的時候不會專門去配置強緩存,而瀏覽器又默認會緩存圖片,css和js等靜態資源,所以開發環境下經常會因為強緩存導致資源沒有及時更新而看不到最新的效果,解決這個問題的方法有很多,常用的有以下幾種:
1)直接ctrl+f5,這個辦法能解決頁面直接引用的資源更新的問題;
2)使用ctrl+shift+delete清除緩存;
3)如果用的是chrome,可以F12在network那里把緩存給禁掉(這是個非常有效的方法):
4)在開發階段,給資源加上一個動態的參數,如css/index.css?v=0.0001,由於每次資源的修改都要更新引用的位置,同時修改參數的值,所以操作起來不是很方便,一般使用前端的構建工具來修改這個參數或 在動態頁面比如jsp里開發就可以用服務器變量來解決(v=${sysRnd});
1.原生寫法
function addVersion(asset){ asset.forEach(function(item,index){ if(item.indexOf('.js') != -1){ document.write('<script src="'+item+'?v='+ (new Date().getTime()) +'"><\/script>'); }else if(item.indexOf('.css') != -1){ document.write('<link rel="stylesheet" href="'+item+'?v='+(new Date().getTime())+'">'); } }); }
2.采用gulp插件
(1)gulp-rev-append
(2)gulp-rev和gulp-rev-collector也能實現同樣的功能
// 修改html和css文件,給靜態文件打戳 gulp.task('stamp', function(){ gulp.src(['rev/*.json', dest.css + "**/*.css"]). pipe(revCollector({ replaceReved: true })). // 修改為 ?v=stamp 形式 pipe(replace(/\-([0-9a-z]{8,})\.(png|jpg|gif|ico)/g, function(a, b, c){ return '.' + c + '?v=' + b; })). pipe(gulp.dest(dest.css)); gulp.src(['rev/*.json', src.html]). pipe(revCollector({ replaceReved: true })). // 修改為 ?v=stamp 形式 pipe(replace(/\-([0-9a-z\-]{8,})\.(css|js)/g, function(a, b, c){ return '.' + c + '?v=' + b; })). pipe(gulp.dest(dest.html)); });
5)如果資源引用的頁面,被嵌入到了一個iframe里面,可以在iframe的區域右鍵單擊重新加載該頁面,以chrome為例:
6)如果緩存問題出現在ajax請求中,最有效的解決辦法就是ajax的請求地址追加隨機數;
7)還有一種情況就是動態設置iframe的src時,有可能也會因為緩存問題,導致看不到最新的效果,這時候在要設置的src后面添加隨機數也能解決問題;
8)如果你用的是grunt和gulp這種前端工具開發,通過它們的插件比如grunt-contrib-connect或gulp-connect來啟動一個靜態服務器,則完全不用擔心開發階段的資源更新問題,因為在這個靜態服務器下的所有資源返回的respone header中,cache-control始終被設置為不緩存:
與之相關的--本地存儲和離線存儲
localStorage/sessionStorage
localStorage.setItem("name", "Robert");
localStorage.getItem("name");
這樣的存取最多可以存儲5M的數據(localStorge在Android webview中不支持擴容,只有在pc瀏覽器中超限才會彈出擴容提示 ),給你更多選擇的空間。但是由於本地存儲是基於字符串的存儲,存儲一串沒有結構的字符串並不是一個理想的選擇。因此,我們可以利用瀏覽器中原生的JSON支持來將JavaScript對象轉化成字符串,從而保存到本地數據中,在讀取的時候也可以將其轉換回JavaScript對象。
緩存和使用圖片的方法
//在本地存儲中保存圖片 var storageFiles = JSON.parse(localStorage.getItem("storageFiles")) || {}, elephant = document.getElementById("elephant"), storageFilesDate = storageFiles.date, date = new Date(), todaysDate = (date.getMonth() + 1).toString() + date.getDate().toString();
// 檢查數據,如果不存在或者數據過期,則創建一個本地存儲 if (typeof storageFilesDate === "undefined" || storageFilesDate < todaysDate) { // 圖片加載完成后執行 elephant.addEventListener("load", function () { var imgCanvas = document.createElement("canvas"), imgContext = imgCanvas.getContext("2d"); // 確保canvas尺寸和圖片一致 imgCanvas.width = elephant.width; imgCanvas.height = elephant.height; // 在canvas中繪制圖片 imgContext.drawImage(elephant, 0, 0, elephant.width, elephant.height); // 將圖片保存為Data URI storageFiles.elephant = imgCanvas.toDataURL("image/png"); storageFiles.date = todaysDate; // 將JSON保存到本地存儲中 try { localStorage.setItem("storageFiles", JSON.stringify(storageFiles)); } catch (e) { console.log("Storage failed: " + e); } }, false); // 設置圖片 elephant.setAttribute("src", "elephant.png"); } else { // Use image from localStorage elephant.setAttribute("src", storageFiles.elephant); }
sessionStorage的數據只存儲到特定的會話中,不屬於持久化的存儲,所以關閉瀏覽器會清除數據。和localstorage具有相同的方法。
離線存儲
設置方法
1. 在HTML5的html標簽中添加一個 manifest="XXX.appcache" 屬性聲明
<!DOCTYPE html> <html manifest="list.appcache">
2.XXX.appcache文件中定義需要緩存的文件清單(里面的資源文件的路徑是相對於manifest的路徑而言的)
CACHE MANIFEST # VERSION 0.3 # 直接緩存的文件 CACHE: # 需要在線訪問的文件 NETWORK: # 替代方案 FALLBACK:
CACHE MANIFEST --(必須) 此標題下列出的文件將在首次下載后進行緩存
#V1.0.2
../addDevice.html
../static/css/reset.css
../static/js/addDevice.js
../static/img/ms1.png
../static/img/clean-face.jpg
NETWORK----(可選)
(1)通配符'*'表示不在CACHE MANIFEST清單里的文件,每次都要重新請求
*
(2)或者指定特定文件,比如login.asp不被離線存儲,每次都要重新發起請求
login.asp
FALLBACK----(可選) 斷網時訪問指定路徑時的替換文件
如斷網時訪問/html5/ 目錄下的所有資源文件,則用 "offline.html" 替代
/html5/ /offline.html
更新原理
更新了manifest文件,瀏覽器會自動的重新下載新的manifest文件並把manifest緩存列表中的所有文件重新請求一次(第二次刷新替換本地緩存為最新緩存),而不是單獨請求某個特定修改過的資源文件,因為manifest是不知道哪個文件被修改過了的。
對於全局更新不必要擔心,因為沒有更新過的資源文件,請求依舊是304響應,只有真正更新過的資源文件才是服務器返回的才是200.
所以控制離線存儲的更新,需要2個步驟,一是更新資源文件,二是更新manifest文件,只要修改manifest文件隨意一處,瀏覽器就會感知manifest文件更新,而我們的資源文件名稱通常是固定的,需要更新manifest文件怎么操作呢?一個比較好的方式是更新以# 開頭的版本號注釋,告訴瀏覽器這個manifest文件被更新過。
manifest資源是滯后靜默更新的
第二次刷新界面之后,才能看到更新后的效果
/*code1,簡單粗暴的*/
applicationCache.onupdateready = function(){ applicationCache.swapCache(); //強制替換緩存 location.reload(); //重新加載頁面 }; /*code2,緩存公用方法*/ // var EventUtil = { // addHandler: function(element, type, handler) { // if (element.addEventListener) { // element.addEventListener(type, handler, false); // } else if (element.attachEvent) { // element.attachEvent("on" + type, handler); // } else { // element["on" + type] = handler; // } // } // }; // EventUtil.addHandler(applicationCache, "updateready", function() { //緩存更新並已下載,要在下次進入頁面生效 // applicationCache.update(); //檢查緩存manifest文件是否更新,ps:頁面加載默認檢查一次。 // applicationCache.swapCache(); //交換到新的緩存項中,交換了要下次進入頁面才生效 // location.reload(); //重新載入頁面 // });
applicationCache 提供了如下的事件:
Event handler Event handler event type
onchecking checking
onerror error
onnoupdate noupdate
ondownloading downloading
onprogress progress
onupdateready updateready
oncached cached
onobsolete obsolete
提供了如下的API:
void update();
// 更新, 但是這個方法適用於一些長期打開的頁面,而不會有刷新動作,比如郵件系統,所以這個就比較適合做自動更新下載
void abort();
// 取消
void swapCache();
// 替換緩存內容 ,對於manifest文件的改變,通常是下一次的刷新才會觸發下載更新,第二次刷新才會切換使用新的緩存文件,通過這個方法,可以強制將緩存替換
注意事項
站點中的其他頁面即使沒有設置manifest屬性,請求的資源如果在緩存中也從緩存中訪問
系統會自動緩存引用清單文件的 HTML 文件
如果manifest文件,或者內部列舉的某一個文件不能正常下載,整個更新過程將視為失敗,瀏覽器繼續全部使用老的緩存
在manifest中使用的相對路徑,相對參照物為manifest文件
站點離線存儲的容量限制是5M
manifest文件中CACHE則與NETWORK,FALLBACK的位置順序沒有關系,如果是隱式聲明需要在最前面
manifest中必須一一聲明文件名,這很令人頭痛
引用manifest的html必須與manifest文件同源,在同一個域下
除此之外,還增加了兩大問題:
(1)PV UV的計算難題,由於當前頁面被強制加入manifest,那么PV 和UV的統計,成了一個難題,因為請求不再是發送到服務器;
(2)緩存對於某個使用manifest的文件,其帶有的參數可能是隨機性的統計參數,如sid=123sss, sid=234fff ,尤其是比如商品詳情的id字段等,這樣每個頁面都自動加入到manifest中,將會帶來很大的存儲開銷,而且是毫無意義的;
所以伴隨而來的,是如何在現有的體系架構下進行數據統計的難題,
對於第一個問題 常規方案是進入離線存儲頁面后自動發出ajax請求,以告知服務器統計PV UV;
對於第二個問題,是將GET請求方式改成POST方式。
離線存儲的適用場景
1.單頁應用
2.對實時性要求不高的業務
3.webApp
參考鏈接
[1]http://www.zhangxinxu.com/wordpress/2013/05/caching-tutorial-for-web-authors-and-webmasters/
[2]http://www.codeceo.com/article/http-cache-control.html
[3]http://www.cnblogs.com/Joans/p/3956490.html
[4]http://web.jobbole.com/86970/
[5]http://www.jianshu.com/p/1a9268594deb
[6]http://blog.techbeta.me/2016/02/http-cache/