文章

浏览器存储方案对比深度解析

浏览器存储方案对比深度解析

一句话概括

全面对比浏览器三大存储方案(localStorage、sessionStorage、IndexedDB)的机制、特性与适用场景,掌握前端本地持久化的核心知识。

背景

前端应用越来越复杂,对本地存储的需求也在不断增长。从最初简单的Cookie,到HTML5引入的Web Storage,再到功能强大的IndexedDB,浏览器提供了多种本地存储方案。理解各方案的设计初衷、能力边界和性能特征,是构建高质量前端应用的基础。

概念与定义

localStorage

持久化的键值对存储,数据以字符串形式保存,关闭浏览器后数据依然保留,除非主动清除。

sessionStorage

会话级别的键值对存储,数据同样以字符串形式保存,但只在当前会话(标签页)有效,关闭标签页后数据清除。

IndexedDB

浏览器内置的NoSQL数据库,支持存储结构化数据(包括文件和Blob),提供事务、索引和游标等完整的数据库能力。

最小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// localStorage
localStorage.setItem('name', '张三');
console.log(localStorage.getItem('name')); // 张三

// sessionStorage
sessionStorage.setItem('token', 'abc123');
console.log(sessionStorage.getItem('token')); // abc123

// IndexedDB
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (e) => {
    const db = e.target.result;
    db.createObjectStore('users', { keyPath: 'id' });
};
request.onsuccess = (e) => {
    const db = e.target.result;
    const tx = db.transaction('users', 'readwrite');
    tx.objectStore('users').add({ id: 1, name: '张三' });
};

核心知识点拆解

1. localStorage详解

存储特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 存储数据(自动转为字符串)
localStorage.setItem('number', 123);          // 存储为 "123"
localStorage.setItem('object', JSON.stringify({ a: 1 })); // 手动序列化

// 读取数据
const num = localStorage.getItem('number');    // "123"(字符串)
const obj = JSON.parse(localStorage.getItem('object')); // { a: 1 }

// 删除数据
localStorage.removeItem('number');

// 清空所有数据
localStorage.clear();

// 获取存储数量
console.log(localStorage.length); // 0

// 遍历所有键
for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    console.log(key, localStorage.getItem(key));
}

关键特性

  • 容量:通常5MB(不同浏览器略有差异)
  • 生命周期:永久存储,除非手动清除
  • 作用域:同源(协议+域名+端口)共享
  • 线程模型:同步操作,会阻塞主线程
  • 存储格式:仅支持字符串,需要手动序列化/反序列化

StorageEvent

1
2
3
4
5
6
7
8
9
10
11
// 监听其他标签页的localStorage变化
window.addEventListener('storage', (e) => {
    console.log('Key:', e.key);            // 变化的键
    console.log('Old:', e.oldValue);       // 旧值
    console.log('New:', e.newValue);       // 新值
    console.log('URL:', e.url);            // 来源页面
    console.log('Storage:', e.storageArea); // 存储对象
});

// 注意:当前页面自身的修改不会触发storage事件
// 只有其他同源页面的修改才会触发

2. sessionStorage详解

存储特性

1
2
3
4
5
6
7
8
9
10
// API与localStorage完全一致
sessionStorage.setItem('tempData', '临时数据');
console.log(sessionStorage.getItem('tempData'));

// 特殊行为:标签页隔离
// 标签页A
sessionStorage.setItem('pageA', 'A的数据');

// 标签页B(同源)
console.log(sessionStorage.getItem('pageA')); // null(无法访问)

关键特性

  • 容量:通常5MB
  • 生命周期:标签页/窗口关闭即清除
  • 作用域:同源 + 同标签页(严格的会话隔离)
  • 标签页复制:通过window.open或链接打开的新标签页会复制一份sessionStorage
  • 线程模型:同步操作

会话概念深入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 场景1:直接打开标签页A
// sessionStorage为空

// 场景2:从标签页A通过链接打开标签页B
// 标签页B会继承标签页A的sessionStorage(副本)
// 但之后两者的修改互不影响

// 场景3:浏览器崩溃恢复
// 部分浏览器会恢复sessionStorage
// Chrome: 恢复前后两个标签页的sessionStorage完全一致
// Firefox: 可能恢复,行为不一致

// 场景4:浏览器前进/后退
// sessionStorage不会丢失,因为标签页没有关闭

3. IndexedDB详解

核心概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 打开/创建数据库
const openRequest = indexedDB.open('MyDatabase', 1);

// 数据库版本升级时触发
openRequest.onupgradeneeded = (event) => {
    const db = event.target.result;
    
    // 创建对象仓库(类似表)
    const store = db.createObjectStore('users', { 
        keyPath: 'id',        // 主键
        autoIncrement: false  // 是否自增
    });
    
    // 创建索引
    store.createIndex('nameIndex', 'name', { 
        unique: false,        // 是否唯一
        multiEntry: false     // 是否多值
    });
    
    store.createIndex('ageIndex', 'age', { unique: false });
};

// 成功打开数据库
openRequest.onsuccess = (event) => {
    const db = event.target.result;
    
    // 添加数据
    const tx = db.transaction('users', 'readwrite');
    const store = tx.objectStore('users');
    
    store.add({ id: 1, name: '张三', age: 25 });
    store.add({ id: 2, name: '李四', age: 30 });
    
    tx.oncomplete = () => console.log('数据添加完成');
};

事务机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 事务的三种模式
// 1. readonly - 只读(默认)
// 2. readwrite - 读写
// 3. versionchange - 版本变更

const db = /* ... */;

// 创建事务
const tx = db.transaction(['users', 'orders'], 'readwrite');

// 事务事件
tx.oncomplete = () => console.log('事务完成');
tx.onerror = () => console.log('事务失败');
tx.onabort = () => console.log('事务中止');

// 事务具有原子性:要么全部成功,要么全部回滚
const store = tx.objectStore('users');
store.add({ id: 3, name: '王五' });
store.add({ id: 4, name: '赵六' });
// 如果第二条失败,第一条也会回滚

索引与游标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 使用索引查询
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const index = store.index('nameIndex');

// 精确查询
const request = index.get('张三');
request.onsuccess = (e) => console.log(e.target.result);

// 范围查询
const range = IDBKeyRange.bound(20, 30);
const rangeRequest = index.openCursor(range);
rangeRequest.onsuccess = (e) => {
    const cursor = e.target.result;
    if (cursor) {
        console.log(cursor.value);
        cursor.continue(); // 继续下一条
    }
};

// 游标方向
// 'next' - 正序(默认)
// 'prev' - 倒序
// 'nextunique' - 正序去重
// 'prevunique' - 倒序去重
const cursorRequest = store.openCursor(null, 'prev');

关键特性

  • 容量:通常为磁盘可用空间的一定比例(可达数百MB甚至GB)
  • 生命周期:永久存储,除非手动清除
  • 作用域:同源共享
  • 线程模型:异步操作,不阻塞主线程
  • 存储格式:支持结构化克隆算法可处理的任意类型
  • 事务支持:ACID特性保证数据一致性

实战案例

案例1:带过期时间的localStorage封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class StorageWithExpiry {
    static set(key, value, expiryMs) {
        const item = {
            value,
            expiry: expiryMs ? Date.now() + expiryMs : null
        };
        localStorage.setItem(key, JSON.stringify(item));
    }
    
    static get(key) {
        const itemStr = localStorage.getItem(key);
        if (!itemStr) return null;
        
        try {
            const item = JSON.parse(itemStr);
            if (item.expiry && Date.now() > item.expiry) {
                localStorage.removeItem(key);
                return null;
            }
            return item.value;
        } catch (e) {
            return null;
        }
    }
    
    static remove(key) {
        localStorage.removeItem(key);
    }
}

// 使用
StorageWithExpiry.set('token', 'abc123', 3600000); // 1小时后过期
const token = StorageWithExpiry.get('token');

案例2:IndexedDB封装工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class IDBHelper {
    constructor(dbName, version, onUpgrade) {
        this.dbName = dbName;
        this.version = version;
        this.onUpgrade = onUpgrade;
        this.db = null;
    }
    
    async open() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(this.dbName, this.version);
            request.onupgradeneeded = (e) => {
                this.onUpgrade(e.target.result);
            };
            request.onsuccess = (e) => {
                this.db = e.target.result;
                resolve(this.db);
            };
            request.onerror = (e) => reject(e.target.error);
        });
    }
    
    async add(storeName, data) {
        const tx = this.db.transaction(storeName, 'readwrite');
        const store = tx.objectStore(storeName);
        return new Promise((resolve, reject) => {
            const request = store.add(data);
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error);
        });
    }
    
    async get(storeName, key) {
        const tx = this.db.transaction(storeName, 'readonly');
        const store = tx.objectStore(storeName);
        return new Promise((resolve, reject) => {
            const request = store.get(key);
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error);
        });
    }
    
    async getAll(storeName) {
        const tx = this.db.transaction(storeName, 'readonly');
        const store = tx.objectStore(storeName);
        return new Promise((resolve, reject) => {
            const request = store.getAll();
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error);
        });
    }
    
    async put(storeName, data) {
        const tx = this.db.transaction(storeName, 'readwrite');
        const store = tx.objectStore(storeName);
        return new Promise((resolve, reject) => {
            const request = store.put(data);
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error);
        });
    }
    
    async delete(storeName, key) {
        const tx = this.db.transaction(storeName, 'readwrite');
        const store = tx.objectStore(storeName);
        return new Promise((resolve, reject) => {
            const request = store.delete(key);
            request.onsuccess = () => resolve();
            request.onerror = () => reject(request.error);
        });
    }
    
    async clear(storeName) {
        const tx = this.db.transaction(storeName, 'readwrite');
        const store = tx.objectStore(storeName);
        return new Promise((resolve, reject) => {
            const request = store.clear();
            request.onsuccess = () => resolve();
            request.onerror = () => reject(request.error);
        });
    }
}

// 使用示例
const dbHelper = new IDBHelper('AppDB', 1, (db) => {
    const store = db.createObjectStore('users', { keyPath: 'id' });
    store.createIndex('nameIndex', 'name', { unique: false });
});

await dbHelper.open();
await dbHelper.add('users', { id: 1, name: '张三', age: 25 });
const user = await dbHelper.get('users', 1);

案例3:离线数据缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class OfflineCache {
    constructor() {
        this.cacheName = 'offline-cache-v1';
    }
    
    // 优先网络,失败回退缓存
    async networkFirst(request) {
        try {
            const response = await fetch(request);
            const clone = response.clone();
            const db = await this.getDB();
            await db.put('responses', {
                url: request.url,
                body: await clone.text(),
                headers: Object.fromEntries(response.headers.entries()),
                timestamp: Date.now()
            });
            return response;
        } catch (e) {
            const cached = await db.get('responses', request.url);
            if (cached) {
                return new Response(cached.body, {
                    headers: cached.headers
                });
            }
            throw e;
        }
    }
    
    // 优先缓存,失败回退网络
    async cacheFirst(request) {
        const db = await this.getDB();
        const cached = await db.get('responses', request.url);
        
        if (cached && Date.now() - cached.timestamp < 3600000) {
            return new Response(cached.body, { headers: cached.headers });
        }
        
        const response = await fetch(request);
        const clone = response.clone();
        await db.put('responses', {
            url: request.url,
            body: await clone.text(),
            headers: Object.fromEntries(response.headers.entries()),
            timestamp: Date.now()
        });
        return response;
    }
}

底层原理

Web Storage的存储引擎

1
2
3
4
5
6
7
8
9
10
浏览器存储架构:
┌─────────────────────────────────────┐
│            渲染进程                   │
│  ┌───────────┐  ┌──────────────────┐ │
│  │  JS线程    │  │   存储线程       │ │
│  │           │→│                  │ │
│  │ localStorage │  SQLite/LevelDB │ │
│  │ sessionStorage│                 │ │
│  └───────────┘  └──────────────────┘ │
└─────────────────────────────────────┘
  1. Chrome:使用LevelDB作为后端存储引擎
  2. Firefox:使用SQLite作为后端存储引擎
  3. Safari:使用SQLite作为后端存储引擎

IndexedDB的架构

1
2
3
4
5
6
7
8
┌──────────────────────────────────────────┐
│                渲染进程                    │
│  ┌─────────┐  ┌──────────┐  ┌─────────┐ │
│  │  JS线程  │→│  IPC桥接  │→│ DB线程   │ │
│  │         │  │          │  │ SQLite  │ │
│  │ IDB API │  │  异步通信  │  │/LevelDB │ │
│  └─────────┘  └──────────┘  └─────────┘ │
└──────────────────────────────────────────┘

结构化克隆算法

IndexedDB使用结构化克隆算法序列化数据,支持以下类型:

  • 基本类型:number、string、boolean、null、undefined
  • 引用类型:Object、Array、Map、Set、Date、RegExp
  • 二进制数据:ArrayBuffer、Blob、File、FileList
  • 特殊类型:ImageData、ArrayBufferView

不支持以下类型:

  • 函数(Function)
  • DOM节点
  • Error对象
  • 某些内置对象的特定属性

存储配额管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 查询存储配额
async function checkStorageQuota() {
    if (navigator.storage && navigator.storage.estimate) {
        const estimate = await navigator.storage.estimate();
        console.log('已使用:', (estimate.usage / 1024 / 1024).toFixed(2), 'MB');
        console.log('总配额:', (estimate.quota / 1024 / 1024).toFixed(2), 'MB');
        console.log('使用率:', ((estimate.usage / estimate.quota) * 100).toFixed(2), '%');
    }
}

// 请求持久化存储(防止浏览器自动清理)
async function requestPersistentStorage() {
    if (navigator.storage && navigator.storage.persist) {
        const granted = await navigator.storage.persist();
        console.log('持久化存储:', granted ? '已授权' : '未授权');
    }
}

高频面试题解析

题目1:localStorage和sessionStorage的区别?

| 特性 | localStorage | sessionStorage | |——|————-|—————-| | 生命周期 | 永久 | 会话级别 | | 作用域 | 同源共享 | 同源+同标签页 | | 容量 | ~5MB | ~5MB | | API | 相同 | 相同 | | 存储位置 | 磁盘 | 磁盘 | | 标签页共享 | 是 | 否 | | 浏览器重启 | 保留 | 清除 |

题目2:localStorage存储满了怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function safeSetItem(key, value) {
    try {
        localStorage.setItem(key, value);
    } catch (e) {
        if (e.name === 'QuotaExceededError') {
            // 策略1:清理过期数据
            cleanExpiredItems();
            try {
                localStorage.setItem(key, value);
            } catch (e2) {
                // 策略2:降级到IndexedDB
                fallbackToIndexedDB(key, value);
            }
        }
    }
}

function cleanExpiredItems() {
    const now = Date.now();
    const keys = Object.keys(localStorage);
    
    for (const key of keys) {
        try {
            const item = JSON.parse(localStorage.getItem(key));
            if (item.expiry && item.expiry < now) {
                localStorage.removeItem(key);
            }
        } catch (e) {
            // 忽略非JSON数据
        }
    }
}

题目3:如何实现跨标签页通信?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 方案1:localStorage + storage事件
window.addEventListener('storage', (e) => {
    if (e.key === 'message') {
        console.log('收到消息:', e.newValue);
    }
});

// 发送消息
localStorage.setItem('message', JSON.stringify({
    type: 'notification',
    data: 'Hello from Tab A',
    timestamp: Date.now()
}));

// 方案2:BroadcastChannel(更优雅)
const channel = new BroadcastChannel('app-channel');
channel.onmessage = (e) => {
    console.log('收到消息:', e.data);
};
channel.postMessage({ type: 'update', data: '新数据' });

// 方案3:SharedWorker
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = (e) => {
    console.log('收到消息:', e.data);
};
worker.port.postMessage({ type: 'ping' });

总结与扩展

方案选择指南

| 需求场景 | 推荐方案 | |———-|———-| | 少量配置数据 | localStorage | | 临时会话数据 | sessionStorage | | 大量结构化数据 | IndexedDB | | 离线缓存 | Cache API + IndexedDB | | 跨标签页通信 | BroadcastChannel | | 敏感数据 | 不存本地,使用内存 |

安全注意事项

  1. XSS风险:localStorage可被JS直接读取,不要存储敏感信息
  2. CSRF风险:Cookie需设置HttpOnly和SameSite
  3. 数据验证:读取存储数据时要验证完整性
  4. 数据清理:定期清理过期数据,避免存储膨胀

未来趋势

  1. Storage API:更精细的配额管理
  2. File System Access API:文件系统级存储
  3. OPFS(Origin Private File System):高性能文件系统
  4. WebNN + 本地模型缓存:AI模型本地存储需求增长

浏览器存储方案的选择没有银弹,需要根据数据量、数据类型、生命周期、性能要求和安全需求综合考量。掌握各方案的底层原理和边界条件,才能在实际项目中做出最优决策。

本文由作者按照 CC BY 4.0 进行授权