時(shí)間:2017-03-27 18:02:47
該文章為 《HTML5緩存機(jī)制淺析:移動(dòng)端Web加載性能優(yōu)化》 的讀書(shū)筆記,整理一下自己的收獲另伍。
一粥喜、HTML5緩存機(jī)制介紹
HTML5是新一代的HTML標(biāo)準(zhǔn)者祖,加入很多新的特性。離線存儲(chǔ)(也可稱為緩存機(jī)制)是其中一個(gè)非常重要的特性瘤旨。HTML5引入的離線存儲(chǔ),這意味著Web應(yīng)用可進(jìn)行緩存竖伯,并可在沒(méi)有因特網(wǎng)連接時(shí)進(jìn)行訪問(wèn)存哲。
1.1 HTML5應(yīng)用程序緩存帶來(lái)的優(yōu)勢(shì)
- 離線瀏覽
- 提高加載速度(從緩存中加載)
- 減少服務(wù)器負(fù)載(瀏覽器將只從服務(wù)器下載更新的資源,強(qiáng)緩存的則不發(fā)起HTTP請(qǐng)求七婴,協(xié)商緩存會(huì)發(fā)起HTTP祟偷,和服務(wù)器驗(yàn)證文件是否修改過(guò)了)
1.2 現(xiàn)有的HTML5緩存機(jī)制
勾選的,表示熟悉
- [x] 瀏覽器緩存機(jī)制(強(qiáng)緩存打厘,協(xié)商緩存)
- [x] DOM Storgage 存儲(chǔ)機(jī)制
- [ ] Web SQL DataBase 存儲(chǔ)機(jī)制 【關(guān)系型數(shù)據(jù)庫(kù)】
- [ ] Application Cache (App Cache)機(jī)制
- [ ] Indexed DataBase (IndexedDB) 【NoSQL】
- [ ] File System API
Web SQL DataBase 官方的標(biāo)準(zhǔn)文檔不在推薦使用修肠,將來(lái)也不再維護(hù),(2015年12月);
-
各大桌面瀏覽器和移動(dòng)端瀏覽器都有很好的實(shí)現(xiàn)這個(gè)接口婚惫,兼容性問(wèn)題不大氛赐,底層基本都是 sqlite(正因?yàn)檫@樣魂爪,作為一個(gè)web標(biāo)準(zhǔn)是不可接受的)
- 經(jīng)測(cè)試,IOS上容量最大支持50M艰管,不過(guò)用系統(tǒng)自帶的safari滓侍,超過(guò)5MB,會(huì)主動(dòng)提醒用戶是否要增加數(shù)據(jù)庫(kù)大小牲芋,不友好(不過(guò)微信里面不會(huì))
- 5MB 如果用來(lái)存一些日志撩笆,還是夠的(當(dāng)然需要記得清理舊的日志)
- websql 目前是主要的解決方案(騰訊用來(lái)做日志記錄,方便排查用戶的錯(cuò)誤反饋)
Indexed DataBase 是下一代的客服端結(jié)構(gòu)化數(shù)據(jù)持久存儲(chǔ)反感缸浦,目前各大瀏覽器中也有很好的支持夕冲,是未來(lái)用來(lái)替換 websql 的方案。
等下查下最新的情況裂逐。
這里只是官網(wǎng)推薦的比較歹鱼,具體使用還需要看目前業(yè)界上的情況。
比如websql目前還是主流的卜高,indexdb是未來(lái)用來(lái)替換的弥姻,具體需要多久,還需要等等掺涛。
1.3 目標(biāo)
- [ ] 分析各種緩存機(jī)制的原理庭敦,用法,特點(diǎn)
- [ ] Android移動(dòng)端Web性能加載優(yōu)化
- [ ] 如何利用緩存機(jī)制提高Web加載性能
二薪缆、分析各個(gè)緩存機(jī)制
先對(duì)各個(gè)緩存機(jī)制有一個(gè)大體的了解秧廉,然后才能去考慮如何優(yōu)化。目前對(duì) 瀏覽器緩存和DOM Storage比較熟悉拣帽,其他的大體了解疼电,但是沒(méi)有項(xiàng)目中用過(guò)。
2.1 瀏覽器緩存機(jī)制
瀏覽器緩存機(jī)制是指通過(guò)HTTP協(xié)議頭里的 Cache-Control / Expires 和 Last-Modified / ETag 等字段控制文件緩存的機(jī)制诞外。
和 DOM Storage, AppCache 等緩存機(jī)制澜沟,本質(zhì)一樣。 一個(gè)在HTTP協(xié)議層實(shí)現(xiàn)(瀏覽器緩存)峡谊,一個(gè)在應(yīng)用層實(shí)現(xiàn)茫虽。
2.1.1 強(qiáng)緩存
采用 Cache-Control 和 Expires 來(lái)控制緩存。 Chrome開(kāi)發(fā)者工具NetWork既们, 查看為 form disk cache
-
Cache-Control (HTTP1.1標(biāo)準(zhǔn)中新增的字段)
- 控制文件本地緩存的有效時(shí)間.
-
Cache-Control:max-age=600
表示文件本地緩存有效時(shí)間600s濒析,接下來(lái)600s內(nèi)請(qǐng)求這個(gè)資源,瀏覽器不發(fā)出HTTP請(qǐng)求啥纸,直接從本地緩存拿 - 是一個(gè)相對(duì)的時(shí)間
- Expires (HTTP1.0標(biāo)準(zhǔn)中的字段)
-
Expires: Thu, 10 Nov 2015 08:45:11 GMT
表示在這個(gè)時(shí)間前号杏,緩存有效 - 是一個(gè)絕對(duì)時(shí)間,由于服務(wù)器的時(shí)間和客服端的時(shí)間可能不一致造成緩存問(wèn)題,因此引入了HTTP1.1的Cache-Control
- 優(yōu)先級(jí)小于 Cache-Control
-
2.1.2 協(xié)商緩存
HTTP 的狀態(tài)為 304 表示 協(xié)商緩存
-
Last-Modified 和 If-Modified-Since
Last-Modified是標(biāo)識(shí)文件在服務(wù)器上的最新更新時(shí)間盾致。下次請(qǐng)求時(shí)主经,如果文件緩存過(guò)期,瀏覽器通過(guò)If-Modified-Since字段帶上這個(gè)時(shí)間庭惜,發(fā)送給服務(wù)器罩驻,由服務(wù)器比較時(shí)間戳來(lái)判斷文件是否有修改。如果沒(méi)有修改护赊,服務(wù)器返回304告訴瀏覽器繼續(xù)使用緩存惠遏;如果有修改,則返回200骏啰,同時(shí)返回最新的文件节吮。
- ETag 和 If-None-Match
Etag也是和Last-Modified一樣,對(duì)文件進(jìn)行標(biāo)識(shí)的字段判耕。不同的是透绩,Etag的取值是一個(gè)對(duì)文件進(jìn)行標(biāo)識(shí)的特征字串。在向服務(wù)器查詢文件是否有更新時(shí)壁熄,瀏覽器通過(guò)If-None-Match字段把特征字串發(fā)送給服務(wù)器渺贤,由服務(wù)器和文件最新特征字串進(jìn)行匹配,來(lái)判斷文件是否有更新请毛。沒(méi)有更新回包304,有更新回包200瞭亮。
Etag和Last-Modified可根據(jù)需求使用一個(gè)或兩個(gè)同時(shí)使用方仿。兩個(gè)同時(shí)使用時(shí),只要滿足基中一個(gè)條件统翩,就認(rèn)為文件沒(méi)有更新仙蚜。
2.1.3 F5 和 Ctrl+F5 的特殊情況
手動(dòng)刷新頁(yè)面(F5),瀏覽器會(huì)直接認(rèn)為緩存已經(jīng)過(guò)期(可能緩存還沒(méi)有過(guò)期)厂汗,在請(qǐng)求中加上字段:Cache-Control:max-age=0委粉,發(fā)包向服務(wù)器查詢是否有文件是否有更新。
可能存在304協(xié)商緩存
強(qiáng)制刷新頁(yè)面(Ctrl+F5)娶桦,瀏覽器會(huì)直接忽略本地的緩存(有緩存也會(huì)認(rèn)為本地沒(méi)有緩存)贾节,在請(qǐng)求中加上字段:Cache-Control:no-cache(或 Pragma:no-cache),發(fā)包向服務(wù)重新拉取文件衷畦。
不存在強(qiáng)緩存和協(xié)商緩存
三栗涂、LocalStorage, SessionStorage
一個(gè)簡(jiǎn)單的鍵值對(duì)存儲(chǔ)系統(tǒng)祈争,接口簡(jiǎn)單實(shí)用斤程,兼容性也很好。有大小限制菩混,提供5M(不同瀏覽器可能不同忿墅,區(qū)分host扁藕, cookie 只有 4k)
3.1 LocalStorage和 SessionStorage的用法
interface Storage {
readonly attribute unsigned long length;
[IndexGetter] DOMString key(in unsigned long index);
[NameGetter] DOMString getItem(in DOMString key);
[NameSetter] void setItem(in DOMString key, in DOMString data);
[NameDeleter] void removeItem(in DOMString key);
void clear();
};
3.2 LocalStorage 和 SessionStorage 的區(qū)別
LocalStorage 和 SessionStorage 的區(qū)別, SessionStorage 在頁(yè)面選項(xiàng)卡關(guān)閉后疚脐,就不存在了亿柑, 但是 LocalStorage會(huì)還存在。但是注意:頁(yè)面刷新的時(shí)候亮曹,SessionStorage還是存在的橄杨。
比如在 頁(yè)面刷新后,表單填寫(xiě)的內(nèi)容還存在照卦,這個(gè)時(shí)候使用 SessionStorage最有用了式矫。
3.3 總結(jié)
分析:Dom Storage給Web提供了一種更錄活的數(shù)據(jù)存儲(chǔ)方式,存儲(chǔ)空間更大(相對(duì)Cookies)役耕,用法也比較簡(jiǎn)單采转,方便存儲(chǔ)服務(wù)器或本地的一些臨時(shí)數(shù)據(jù)。
- 適合存儲(chǔ)簡(jiǎn)單數(shù)據(jù)
- 如果存結(jié)構(gòu)化數(shù)據(jù)瞬痘,可以借住JSON的功能
- 不適合存儲(chǔ)復(fù)雜故慈,存儲(chǔ)空間要求比較大的數(shù)據(jù),還有不適合存靜態(tài)文件
四框全、Web SQL Database存儲(chǔ)機(jī)制
目前用來(lái)存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù)還是比較主流的一個(gè)選擇察绷。 參考 《騰訊開(kāi)發(fā)工程師:前端異常監(jiān)控到底怎么做》
IOS下,最大支持50M , 系統(tǒng)自帶瀏覽器Safari津辩,默認(rèn)超過(guò)5M的時(shí)候拆撼,會(huì)彈出提示讓用戶增加大小,不太友好喘沿。
微信瀏覽器下闸度,不會(huì)提示。
4.1 簡(jiǎn)單例子
<script>
if(window.openDatabase){
//打開(kāi)數(shù)據(jù)庫(kù)蚜印,如果沒(méi)有則創(chuàng)建
var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024);
//通過(guò)事務(wù)莺禁,創(chuàng)建一個(gè)表,并添加兩條記錄
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")');
});
//查詢表中所有記錄窄赋,并展示出來(lái)
db.transaction(function (tx) {
tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) {
var len = results.rows.length, i;
msg = "<p>Found rows: " + len + "</p>";
for(i=0; i<len; i++){
msg += "<p>" + results.rows.item(i).log + "</p>";
}
document.querySelector('#status').innerHTML = msg;
}, null);
});
}
</script>
<div id="status" name="status">Status Message</div><br>
4.2 分析
- 適合存儲(chǔ)結(jié)構(gòu)復(fù)雜的數(shù)據(jù)
- 使用起來(lái)相對(duì)麻煩點(diǎn)哟冬,需要了解SQL語(yǔ)句
- 不是和做靜態(tài)文件的存儲(chǔ)
五、Application Cache 機(jī)制
似乎是為了支持 Web App 離線使用二開(kāi)發(fā)的緩存機(jī)制忆绰。 緩存 和 瀏覽器緩存機(jī)制類似柒傻。
- 按文件單位進(jìn)行緩存
- 通過(guò)manifest來(lái)控制文件的緩存
- 大小5M的限制
AppCache的原理有兩個(gè)關(guān)鍵點(diǎn):manifest屬性和 manifest文件。
- 緩存文件有更新需要更新 manifest文件 [正常有更新文件就修改mainfest里面的一個(gè)版本號(hào)]
CACHE MANIFEST
# 2012-02-21 v1.0.0
/theme.css
/logo.gif
/main.js
NETWORK:
login.asp
FALLBACK:
/html/ /offline.html <br>
5.2 分析
分析:AppCache看起來(lái)是一種比較好的緩存方法较木,除了緩存靜態(tài)資源文件外红符,也適合構(gòu)建Web離線 App。在實(shí)際使用中有些需要注意的地方,有一些可以說(shuō)是”坑“预侯。
要更新緩存的文件致开,需要更新包含它的manifest文件,那怕只加一個(gè)空格萎馅。常用的方法双戳,是修改manifest文件注釋中的版本號(hào)。如:# 2012-02-21 v1.0.0糜芳;
被緩存的文件飒货,瀏覽器是先使用,再通過(guò)檢查manifest文件是否有更新來(lái)更新緩存文件峭竣。這樣緩存文件可能用的不是最新的版本塘辅。
在更新緩存過(guò)程中,如果有一個(gè)文件更新失敗皆撩,則整個(gè)更新會(huì)失敗扣墩。
manifest和引用它的HTML要在相同Host。
manifest文件中的文件列表扛吞,如果是相對(duì)路徑呻惕,則是相對(duì)manifest文件的相對(duì)路徑。
manifest也有可能更新出錯(cuò)滥比,導(dǎo)致緩存文件更新失敗亚脆。
沒(méi)有緩存的資源在已經(jīng)緩存的HTML中不能加載,即使有網(wǎng)絡(luò)盲泛。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/型酥。
manifest文件本身不能被緩存,且manifest文件的更新使用的是瀏覽器緩存機(jī)制查乒。所以manifest
文件的Cache-Control緩存時(shí)間不能設(shè)置太長(zhǎng)。
另外郁竟,根據(jù)官方文檔玛迄,AppCache已經(jīng)不推薦使用了,標(biāo)準(zhǔn)也不會(huì)再支持∨锬叮現(xiàn)在主流的瀏覽器都是還支持AppCache的蓖议,以后就不太確定了。
六讥蟆、 Indexed Database
IndexedDB也是一種數(shù)據(jù)庫(kù)的存儲(chǔ)機(jī)制勒虾,但不同于已經(jīng)不再支持的Web SQL Database。IndexedDB不是傳統(tǒng)的關(guān)系數(shù)據(jù)庫(kù)瘸彤,可歸為NoSQL數(shù)據(jù)庫(kù)修然。IndexedDB又類似于Dom Storage的key-value的存儲(chǔ)方式,但功能更強(qiáng)大,且存儲(chǔ)空間更大愕宋。
- NoSQL數(shù)據(jù)庫(kù)
- 類似DOM Storage 鍵值對(duì)存儲(chǔ)方式
6.1 基本使用
<script type="text/javascript">
var db;
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
//瀏覽器是否支持IndexedDB
if (window.indexedDB) {
//打開(kāi)數(shù)據(jù)庫(kù)玻靡,如果沒(méi)有,則創(chuàng)建
var openRequest = window.indexedDB.open("people_db", 1);
//DB版本設(shè)置或升級(jí)時(shí)回調(diào)
openRequest.onupgradeneeded = function(e) {
console.log("Upgrading...");
var thisDB = e.target.result;
if(!thisDB.objectStoreNames.contains("people")) {
console.log("Create Object Store: people.");
//創(chuàng)建存儲(chǔ)對(duì)象中贝,類似于關(guān)系數(shù)據(jù)庫(kù)的表
thisDB.createObjectStore("people", { autoIncrement:true });
//創(chuàng)建存儲(chǔ)對(duì)象囤捻, 還創(chuàng)建索引
//var objectStore = thisDB.createObjectStore("people",{ autoIncrement:true });
// //first arg is name of index, second is the path (col);
//objectStore.createIndex("name","name", {unique:false});
//objectStore.createIndex("email","email", {unique:true});
}
}
//DB成功打開(kāi)回調(diào)
openRequest.onsuccess = function(e) {
console.log("Success!");
//保存全局的數(shù)據(jù)庫(kù)對(duì)象,后面會(huì)用到
db = e.target.result;
//綁定按鈕點(diǎn)擊事件
document.querySelector("#addButton").addEventListener("click", addPerson, false);
document.querySelector("#getButton").addEventListener("click", getPerson, false);
document.querySelector("#getAllButton").addEventListener("click", getPeople, false);
document.querySelector("#getByName").addEventListener("click", getPeopleByNameIndex1, false);
}
//DB打開(kāi)失敗回調(diào)
openRequest.onerror = function(e) {
console.log("Error");
console.dir(e);
}
}else{
alert('Sorry! Your browser doesn\'t support the IndexedDB.');
}
//添加一條記錄
function addPerson(e) {
var name = document.querySelector("#name").value;
var email = document.querySelector("#email").value;
console.log("About to add "+name+"/"+email);
var transaction = db.transaction(["people"],"readwrite");
var store = transaction.objectStore("people");
//Define a person
var person = {
name:name,
email:email,
created:new Date()
}
//Perform the add
var request = store.add(person);
//var request = store.put(person, 2);
request.onerror = function(e) {
console.log("Error",e.target.error.name);
//some type of error handler
}
request.onsuccess = function(e) {
console.log("Woot! Did it.");
}
}
//通過(guò)KEY查詢記錄
function getPerson(e) {
var key = document.querySelector("#key").value;
if(key === "" || isNaN(key)) return;
var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var request = store.get(Number(key));
request.onsuccess = function(e) {
var result = e.target.result;
console.dir(result);
if(result) {
var s = "<p><h2>Key "+key+"</h2></p>";
for(var field in result) {
s+= field+"="+result[field]+"<br/>";
}
document.querySelector("#status").innerHTML = s;
} else {
document.querySelector("#status").innerHTML = "<h2>No match!</h2>";
}
}
}
//獲取所有記錄
function getPeople(e) {
var s = "";
db.transaction(["people"], "readonly").objectStore("people").openCursor().onsuccess = function(e) {
var cursor = e.target.result;
if(cursor) {
s += "<p><h2>Key "+cursor.key+"</h2></p>";
for(var field in cursor.value) {
s+= field+"="+cursor.value[field]+"<br/>";
}
s+="</p>";
cursor.continue();
}
document.querySelector("#status2").innerHTML = s;
}
}
//通過(guò)索引查詢記錄
function getPeopleByNameIndex(e)
{
var name = document.querySelector("#name1").value;
var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var index = store.index("name");
//name is some value
var request = index.get(name);
request.onsuccess = function(e) {
var result = e.target.result;
if(result) {
var s = "<p><h2>Name "+name+"</h2><p>";
for(var field in result) {
s+= field+"="+result[field]+"<br/>";
}
s+="</p>";
} else {
document.querySelector("#status3").innerHTML = "<h2>No match!</h2>";
}
}
}
//通過(guò)索引查詢記錄
function getPeopleByNameIndex1(e)
{
var s = "";
var name = document.querySelector("#name1").value;
var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var index = store.index("name");
//name is some value
index.openCursor().onsuccess = function(e) {
var cursor = e.target.result;
if(cursor) {
s += "<p><h2>Key "+cursor.key+"</h2></p>";
for(var field in cursor.value) {
s+= field+"="+cursor.value[field]+"<br/>";
}
s+="</p>";
cursor.continue();
}
document.querySelector("#status3").innerHTML = s;
}
}
</script>
<p>添加數(shù)據(jù)<br/>
<input type="text" id="name" placeholder="Name"><br/>
<input type="email" id="email" placeholder="Email"><br/>
<button id="addButton">Add Data</button>
</p>
<p>根據(jù)Key查詢數(shù)據(jù)<br/>
<input type="text" id="key" placeholder="Key"><br/>
<button id="getButton">Get Data</button>
</p>
<div id="status" name="status"></div>
<p>獲取所有數(shù)據(jù)<br/>
<button id="getAllButton">Get EveryOne</button>
</p>
<div id="status2" name="status2"></div>
<p>根據(jù)索引:Name查詢數(shù)據(jù)<br/>
<input type="text" id="name1" placeholder="Name"><br/>
<button id="getByName">Get ByName</button>
</p>
<div id="status3" name="status3"></div><br>
6.2 分析
分析:IndexedDB是一種靈活且功能強(qiáng)大的數(shù)據(jù)存儲(chǔ)機(jī)制邻寿,它集合了Dom Storage和Web SQL Database的優(yōu)點(diǎn)蝎土,用于存儲(chǔ)大塊或復(fù)雜結(jié)構(gòu)的數(shù)據(jù),提供更大的存儲(chǔ)空間绣否,使用起來(lái)也比較簡(jiǎn)單誊涯。可以作為Web SQL Database的替代枝秤。不太適合靜態(tài)文件的緩存醋拧。
以key-value 的方式存取對(duì)象,可以是任何類型值或?qū)ο蟮淼ǘM(jìn)制丹壕。
可以對(duì)對(duì)象任何屬性生成索引,方便查詢薇溃。
較大的存儲(chǔ)空間菌赖,默認(rèn)推薦250MB(分Host),比Dom Storage的5MB要大得多沐序。
通過(guò)數(shù)據(jù)庫(kù)的事務(wù)(tranction)機(jī)制進(jìn)行數(shù)據(jù)操作琉用,保證數(shù)據(jù)一致性。
異步的 API 調(diào)用策幼,避免造成等待而影響體驗(yàn)邑时。
七、File System API
- HTML5新加入的存儲(chǔ)機(jī)制
- 為 webApp提供了虛擬的文件系統(tǒng)特姐,類似Native App訪問(wèn)本地系統(tǒng)一樣
7.1 File System 的優(yōu)勢(shì)
- 滿足大塊的二進(jìn)制文件存儲(chǔ)
- 通過(guò)預(yù)加載資源文件提供性能
- 可以直接編輯文件
7.2 兩種文件空間
- 臨時(shí)型
由瀏覽器自動(dòng)分配的晶丘,但可能被瀏覽器收回
- 持久性
需要顯示的申請(qǐng),申請(qǐng)時(shí)瀏覽器會(huì)給用戶提示唐含,讓用戶確定浅浮。瀏覽器不會(huì)收回(提供用戶則不太友好),大小不夠用需要再次申請(qǐng)捷枯。
7.3 分析
File System API給Web App帶來(lái)了文件系統(tǒng)的功能滚秩,Native文件系統(tǒng)的功能在Web App中都有相應(yīng)的實(shí)現(xiàn)。任何需要通過(guò)文件來(lái)管理數(shù)據(jù)淮捆,或通過(guò)文件系統(tǒng)進(jìn)行數(shù)據(jù)管理的場(chǎng)景都比較適合郁油。