隨著移動網絡的發展與演化,我們手機上現在除了有原生 App,還能跑“WebApp”——它即開即用,用完即走。一個優秀的 WebApp 甚至可以擁有和原生 App 媲美的功能和體驗。
我認為,WebApp 就是我們前端性能優化的產物,是我們前端工程師對體驗不懈追求的結果,是 Web 網頁在性能上向 Native 應用的一次“宣戰”。
WebApp 優異的性能表現,要歸功于瀏覽器存儲技術的廣泛應用——這其中除了我們上節提到的緩存,本地存儲技術也功不可沒。
## 故事的開始:從 Cookie 說起
Cookie 的本職工作并非本地存儲,而是“維持狀態”。
在 Web 開發的早期,人們亟需解決的一個問題就是狀態管理的問題:HTTP 協議是一個無狀態協議,服務器接收客戶端的請求,返回一個響應,故事到此就結束了,服務器并沒有記錄下關于客戶端的任何信息。那么下次請求的時候,如何讓服務器知道“我是我”呢?
在這樣的背景下,Cookie 應運而生。
Cookie 說白了就是一個存儲在瀏覽器里的一個小小的文本文件,它附著在 HTTP 請求上,在瀏覽器和服務器之間“飛來飛去”。它可以攜帶用戶信息,當服務器檢查 Cookie 的時候,便可以獲取到客戶端的狀態。
關于 Cookie 的詳細內容,我們可以在 Chrome 的 Application 面板中查看到:

如大家所見,**Cookie 以鍵值對的形式存在**。
### Cookie的性能劣勢
#### Cookie 不夠大
大家知道,Cookie 是有體積上限的,它最大只能有 4KB。當 Cookie 超過 4KB 時,它將面臨被裁切的命運。這樣看來,Cookie 只能用來存取少量的信息。
#### 過量的 Cookie 會帶來巨大的性能浪費
**Cookie 是緊跟域名的**。我們通過響應頭里的 Set-Cookie 指定要存儲的 Cookie 值。默認情況下,domain 被設置為設置 Cookie 頁面的主機名,我們也可以手動設置 domain 的值:
```
Set-Cookie: name=xiuyan; domain=xiuyan.me
```
**同一個域名下的所有請求,都會攜帶 Cookie**。大家試想,如果我們此刻僅僅是請求一張圖片或者一個 CSS 文件,我們也要攜帶一個 Cookie 跑來跑去(關鍵是 Cookie 里存儲的信息我現在并不需要),這是一件多么勞民傷財的事情。Cookie 雖然小,請求卻可以有很多,隨著請求的疊加,這樣的不必要的 Cookie 帶來的開銷將是無法想象的。
隨著前端應用復雜度的提高,Cookie 也漸漸演化為了一個“存儲多面手”——它不僅僅被用于維持狀態,還被塞入了一些亂七八糟的其它信息,被迫承擔起了本地存儲的“重任”。在沒有更好的本地存儲解決方案的年代里,Cookie 小小的身體里承載了 4KB 內存所不能承受的壓力。
為了彌補 Cookie 的局限性,讓“專業的人做專業的事情”,Web Storage 出現了。
## 向前一步:Web Storage
Web Storage 是 HTML5 專門為瀏覽器存儲而提供的數據存儲機制。它又分為 Local Storage 與 Session Storage。這兩組概念非常相近,我們不妨先理解它們之間的區別,再對它們的共性進行研究。
### Local Storage 與 Session Storage 的區別
兩者的區別在于**生命周期**與**作用域**的不同。
* 生命周期:Local Storage 是持久化的本地存儲,存儲在其中的數據是永遠不會過期的,使其消失的唯一辦法是手動刪除;而 Session Storage 是臨時性的本地存儲,它是會話級別的存儲,當會話結束(頁面被關閉)時,存儲內容也隨之被釋放。
* 作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特別的一點在于,即便是相同域名下的兩個頁面,只要它們**不在同一個瀏覽器窗口中**打開,那么它們的 Session Storage 內容便無法共享。
### Web Storage 的特性
* 存儲容量大: Web Storage 根據瀏覽器的不同,存儲容量可以達到 5-10M 之間。
* 僅位于瀏覽器端,不與服務端發生通信。
### Web Storage 核心 API 使用示例
Web Storage 保存的數據內容和 Cookie 一樣,是文本內容,以鍵值對的形式存在。Local Storage 與 Session Storage 在 API 方面無異,這里我們以 localStorage 為例:
* 存儲數據:setItem()
```
localStorage.setItem('user_name', 'xiuyan')
```
* 讀取數據: getItem()
```
localStorage.getItem('user_name')
```
* 刪除某一鍵名對應的數據: removeItem()
```
localStorage.removeItem('user_name')
```
* 清空數據記錄:clear()
```
localStorage.clear()
```
### 應用場景
#### Local Storage
Local Storage 在存儲方面沒有什么特別的限制,理論上 Cookie 無法勝任的、可以用簡單的鍵值對來存取的數據存儲任務,都可以交給 Local Storage 來做。
這里給大家舉個例子,考慮到 Local Storage 的特點之一是**持久**,有時我們更傾向于用它來存儲一些內容穩定的資源。比如圖片內容豐富的電商網站會用它來存儲 Base64 格式的圖片字符串:

有的網站還會用它存儲一些不經常更新的 CSS、JS 等靜態資源。
#### Session Storage
Session Storage 更適合用來存儲生命周期和它同步的**會話級別**的信息。這些信息只適用于當前會話,當你開啟新的會話時,它也需要相應的更新或釋放。比如微博的 Session Storage 就主要是存儲你本次會話的瀏覽足跡:

lasturl 對應的就是你上一次訪問的 URL 地址,這個地址是即時的。當你切換 URL 時,它隨之更新,當你關閉頁面時,留著它也確實沒有什么意義了,干脆釋放吧。這樣的數據用 Session Storage 來處理再合適不過。
這樣看來,Web Storage 確實也夠強大了。那么 Web Storage 是否能 hold 住所有的存儲場景呢?
答案是否定的。大家也看到了,Web Storage 是一個從定義到使用都非常簡單的東西。它使用鍵值對的形式進行存儲,這種模式有點類似于對象,卻甚至連對象都不是——它只能存儲字符串,要想得到對象,我們還需要先對字符串進行一輪解析。
說到底,Web Storage 是對 Cookie 的拓展,它只能用于存儲少量的簡單數據。當遇到大規模的、結構復雜的數據時,Web Storage 也愛莫能助了。這時候我們就要清楚我們的終極大 boss——IndexDB!
## 終極形態:IndexDB
IndexDB 是一個**運行在瀏覽器上的非關系型數據庫**。既然是數據庫了,那就不是 5M、10M 這樣小打小鬧級別了。理論上來說,IndexDB 是沒有存儲上限的(一般來說不會小于 250M)。它不僅可以存儲字符串,還可以存儲二進制數據。
IndexDB 從推出之日起,其優質教程就層出不絕,我們今天不再著重講解它的詳細操作。接下來,我們遵循 MDN 推薦的操作模式,通過一個基本的 IndexDB 使用流程,旨在對 IndexDB 形成一個感性的認知:
1. 打開/創建一個 IndexDB 數據庫(當該數據庫不存在時,open 方法會直接創建一個名為 xiaoceDB 新數據庫)。
```
// 后面的回調中,我們可以通過event.target.result拿到數據庫實例
let db
// 參數1位數據庫名,參數2為版本號
const request = window.indexedDB.open("xiaoceDB", 1)
// 使用IndexDB失敗時的監聽函數
request.onerror = function(event) {
console.log('無法使用IndexDB')
}
// 成功
request.onsuccess = function(event){
// 此處就可以獲取到db實例
db = event.target.result
console.log("你打開了IndexDB")
}
```
2. 創建一個 object store(object store 對標到數據庫中的“表”單位)。
```
// onupgradeneeded事件會在初始化數據庫/版本發生更新時被調用,我們在它的監聽函數中創建object store
request.onupgradeneeded = function(event){
let objectStore
// 如果同名表未被創建過,則新建test表
if (!db.objectStoreNames.contains('test')) {
objectStore = db.createObjectStore('test', { keyPath: 'id' })
}
}
```
3. 構建一個事務來執行一些數據庫操作,像增加或提取數據等。
```
// 創建事務,指定表格名稱和讀寫權限
const transaction = db.transaction(["test"],"readwrite")
// 拿到Object Store對象
const objectStore = transaction.objectStore("test")
// 向表格寫入數據
objectStore.add({id: 1, name: 'xiuyan'})
```
4. 通過監聽正確類型的事件以等待操作完成。
```
// 操作成功時的監聽函數
transaction.oncomplete = function(event) {
console.log("操作成功")
}
// 操作失敗時的監聽函數
transaction.onerror = function(event) {
console.log("這里有一個Error")
}
```
### IndexDB 的應用場景
通過上面的示例大家可以看出,在 IndexDB 中,我們可以創建多個數據庫,一個數據庫中創建多張表,一張表中存儲多條數據——這足以 hold 住復雜的結構性數據。IndexDB 可以看做是 LocalStorage 的一個升級,當數據的復雜度和規模上升到了 LocalStorage 無法解決的程度,我們毫無疑問可以請出 IndexDB 來幫忙。
## 小結
瀏覽器緩存/存儲技術的出現和發展,為我們的前端應用帶來了無限的轉機。近年來基于緩存/存儲技術的第三方庫層出不絕,此外還衍生出了 [PWA](https://lavas.baidu.com/pwa) 這樣優秀的 Web 應用模型。可以說,現代前端應用,尤其是移動端應用,之所以可以發展到在體驗上叫板 Native 的地步,主要就是仰仗緩存/存儲立下的汗馬功勞。
- 開篇:知識體系與小冊格局
- 網絡篇 1:webpack 性能調優與 Gzip 原理
- 網絡篇 2:圖片優化——質量與性能的博弈
- 存儲篇 1:瀏覽器緩存機制介紹與緩存策略剖析
- 存儲篇 2:本地存儲——從 Cookie 到 Web Storage、IndexDB
- 彩蛋篇:CDN 的緩存與回源機制解析
- 渲染篇 1:服務端渲染的探索與實踐
- 渲染篇 2:知己知彼——解鎖瀏覽器背后的運行機制
- 渲染篇 3:對癥下藥——DOM 優化原理與基本實踐
- 渲染篇 4:千方百計——Event Loop 與異步更新策略
- 渲染篇 5:最后一擊——回流(Reflow)與重繪(Repaint)
- 應用篇 1:優化首屏體驗——Lazy-Load 初探
- 應用篇 2:事件的節流(throttle)與防抖(debounce)
- 性能監測篇:Performance、LightHouse 與性能 API
- 前方的路:希望成為你的起點