0%

LocalStorage 性能优化——存储策略与最佳实践

做一个离线应用,需要存储大量的用户数据,浏览器存储限制和性能问题让大家头疼不已。对比各种存储方案,发现了很多有趣的技术细节。

前端存储技术概述

  在现代 Web 应用中,客户端存储已成为提升用户体验的重要手段。从简单的用户偏好设置到复杂的离线数据同步,前端存储技术的选择直接影响应用的性能和可靠性。然而,随着 Web 应用功能的日益复杂,数据量也在快速增长,传统的存储方案面临诸多挑战。

  目前主流的前端存储技术包括 Web Storage(LocalStorage/SessionStorage)、IndexedDB、WebSQL(已废弃)、Cookie 以及 Cache API 等。每种技术都有其适用场景和局限性,在面对大数据存储时,需要根据具体需求选择合适的方案。

存储技术对比

存储类型容量限制数据类型同步/异步事务支持浏览器支持
Cookie4KB/个字符串同步全部
LocalStorage5-10MB字符串同步全部
SessionStorage5-10MB字符串同步全部
IndexedDB几 GB对象、数组、二进制异步支持现代浏览器
Cache API不定Response异步支持现代浏览器

传统存储方案的局限性

LocalStorage 的性能问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// LocalStorage 的基本使用 localStorage.setItem('userPreferences', Json.stringify({
theme: 'dark',
language: 'zh-CN',
notifications: true
}));

const preferences = Json.parse(localStorage.getItem('userPreferences'));

// 大数据存储的问题演示 function demonstrateLocalStorageIssues() {
// 存储大量数据会导致性能问题 const largeData = new Array(10000).fill(0).map((_, i) => ({
id: i,
name: `Item ${i}`,
data: `Data for item ${i} - `.repeat(100)
}));

console.time('localStorage-write');
localStorage.setItem('largeData', Json.stringify(largeData));
console.timeEnd('localStorage-write'); // 可能会很慢 console.time('localStorage-read');
const retrievedData = Json.parse(localStorage.getItem('largeData'));
console.timeEnd('localStorage-read'); // 阻塞主线程

// 清理 localStorage.removeItem('largeData');
}

LocalStorage 的阻塞性质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// LocalStorage 同步操作会阻塞主线程 function localStorageBlockingDemo() {
const startTime = performance.now();

// 存储大量数据会阻塞 UI
const hugeObject = {};
for (let i = 0; i < 100000; i++) {
hugeObject[`key${i}`] = `value${i}-`.repeat(100);
}

// 这个操作可能会冻结 UI 几秒钟 localStorage.setItem('hugeObject', Json.stringify(hugeObject));

const endTime = performance.now();
console.log(`写入耗时: ${endTime - startTime}ms`);

// 读取同样阻塞 console.time('read-huge-object');
const retrieved = Json.parse(localStorage.getItem('hugeObject'));
console.timeEnd('read-huge-object');

// 清理 localStorage.removeItem('hugeObject');
}

IndexedDB 深度解析

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// IndexedDB 数据库初始化 class IndexedDBManager {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}

async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);

request.onerror = (event) => {
reject(request.error);
};

request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};

// 数据库升级或首次创建 request.onupgradeneeded = (event) => {
this.db = event.target.result;

// 创建对象仓库(相当于数据表)
if (!this.db.objectStoreNames.contains('users')) {
const userStore = this.db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});

// 创建索引 userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('age', 'age', { unique: false });
}

if (!this.db.objectStoreNames.contains('preferences')) {
const prefStore = this.db.createObjectStore('preferences', {
keyPath: 'userId'
});
}
};
});
}

// 添加数据 async add(storeName, data) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.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 transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.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, query = null, count = null) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);

const request = query ? store.getAll(query, count) : store.getAll(count);

return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}

// 更新数据 async put(storeName, data) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.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 transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);

return new Promise((resolve, reject) => {
const request = store.delete(key);

request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}

// 使用示例 async function exampleUsage() {
const db = new IndexedDBManager('MyAppDB', 1);
await db.init();

// 添加用户 const userId = await db.add('users', {
name: '张三',
email: 'zhangsan@example.com',
age: 25,
preferences: { theme: 'dark', lang: 'zh-CN' }
});

console.log('用户 ID:', userId);

// 获取用户 const user = await db.get('users', 1);
console.log('用户信息:', user);

// 获取所有用户 const allUsers = await db.getAll('users');
console.log('所有用户:', allUsers);
}

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
class BulkOperations {
constructor(db) {
this.db = db;
}

// 批量插入优化 async bulkInsert(storeName, dataArray) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);

let completed = 0;
const total = dataArray.length;

// 优化:批量操作使用单个事务 for (let i = 0; i < dataArray.length; i++) {
const request = store.add(dataArray[i]);

request.onsuccess = () => {
completed++;
if (completed === total) {
resolve(completed);
}
};

request.onerror = () => {
reject(request.error);
};
}
});
}

// 批量更新 async bulkUpdate(storeName, updates) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);

let completed = 0;
const total = updates.length;

for (let i = 0; i < updates.length; i++) {
const request = store.put(updates[i]);

request.onsuccess = () => {
completed++;
if (completed === total) {
resolve(completed);
}
};

request.onerror = () => {
reject(request.error);
};
}
});
}

// 使用游标进行大数据量查询 async cursorQuery(storeName, indexName, range, callback) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);

const request = index.openCursor(range);
const results = [];

return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
const cursor = event.target.result;

if (cursor) {
// 处理当前项 results.push(cursor.value);

// 根据需要的回调决定是否继续 if (callback && callback(cursor.value, results.length)) {
cursor.continue();
} else {
resolve(results);
}
} else {
resolve(results);
}
};

request.onerror = () => reject(request.error);
});
}
}

大数据存储优化策略

数据分片存储

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
class ShardedStorage {
constructor(db, chunkSize = 1000000) { // 1MB per chunk
this.db = db;
this.chunkSize = chunkSize;
}

// 将大数据分割存储 async storeLargeData(key, data) {
const serializedData = Json.stringify(data);
const chunks = this.splitIntoChunks(serializedData, this.chunkSize);

// 存储元数据 await this.db.put('metadata', {
id: key,
totalChunks: chunks.length,
originalSize: serializedData.length,
timestamp: Date.now()
});

// 存储分片 const promises = chunks.map((chunk, index) =>
this.db.put(`chunks_${Math.floor(index / 100)}`, {
id: `${key}_${index}`,
data: chunk,
chunkIndex: index
})
);

await Promise.all(promises);
}

// 读取大数据 async retrieveLargeData(key) {
// 获取元数据 const metadata = await this.db.get('metadata', key);
if (!metadata) {
throw new Error('数据不存在');
}

// 读取所有分片 const chunks = [];
for (let i = 0; i < metadata.totalChunks; i++) {
const chunkIndex = Math.floor(i / 100);
const chunkKey = `${key}_${i}`;
const chunk = await this.db.get(`chunks_${chunkIndex}`, chunkKey);
chunks.push(chunk.data);
}

const combinedData = chunks.join('');
return Json.parse(combinedData);
}

// 按需加载(懒加载)
async retrieveChunk(key, chunkIndex) {
const chunkIndexGroup = Math.floor(chunkIndex / 100);
const chunkKey = `${key}_${chunkIndex}`;
const chunk = await this.db.get(`chunks_${chunkIndexGroup}`, chunkKey);
return chunk ? Json.parse(chunk.data) : null;
}

splitIntoChunks(str, chunkSize) {
const chunks = [];
for (let i = 0; i < str.length; i += chunkSize) {
chunks.push(str.slice(i, i + chunkSize));
}
return chunks;
}
}

压缩存储

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
93
94
95
96
97
98
// 使用 CompressionStream API(较新浏览器)
class CompressedStorage {
constructor(db) {
this.db = db;
}

async compressAndStore(key, data) {
const jsonString = Json.stringify(data);

// 检查浏览器是否支持 CompressionStream
if ('CompressionStream' in window) {
const stream = new CompressionStream('gzip');
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();

// 写入数据 const encoder = new TextEncoder();
writer.write(encoder.encode(jsonString));
writer.close();

// 读取压缩后的数据 const compressedChunks = [];
let done = false;

while (!done) {
const { value, done: streamDone } = await reader.read();
done = streamDone;
if (value) {
compressedChunks.push(value);
}
}

// 合并所有块 const compressedData = this.concatenateBuffers(compressedChunks);

// 存储压缩数据 await this.db.put('compressed_data', {
id: key,
data: Array.from(compressedData), // 转换为可序列化格式 originalSize: jsonString.length,
compressedSize: compressedData.byteLength,
timestamp: Date.now()
});
} else {
// 降级方案:存储原始数据 await this.db.put('raw_data', {
id: key,
data: jsonString,
timestamp: Date.now()
});
}
}

async decompressAndRetrieve(key) {
const record = await this.db.get('compressed_data', key);
if (!record) {
throw new Error('数据不存在');
}

if ('DecompressionStream' in window) {
const compressedBuffer = new Uint8Array(record.data);

const stream = new DecompressionStream('gzip');
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();

writer.write(compressedBuffer);
writer.close();

const decompressedChunks = [];
let done = false;

while (!done) {
const { value, done: streamDone } = await reader.read();
done = streamDone;
if (value) {
decompressedChunks.push(value);
}
}

const decompressedData = this.concatenateBuffers(decompressedChunks);
const decoder = new TextDecoder();
const decompressedString = decoder.decode(decompressedData);

return Json.parse(decompressedString);
} else {
// 降级方案 const rawRecord = await this.db.get('raw_data', key);
return Json.parse(rawRecord.data);
}
}

concatenateBuffers(buffers) {
const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;

for (const buffer of buffers) {
result.set(buffer, offset);
offset += buffer.length;
}

return result;
}
}

缓存策略

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
class StorageCache {
constructor(db, maxSize = 1000) {
this.db = db;
this.cache = new Map();
this.maxSize = maxSize;
this.accessOrder = []; // 记录访问顺序
}

async get(key) {
// 先检查内存缓存 if (this.cache.has(key)) {
this.updateAccessOrder(key);
return this.cache.get(key);
}

// 从数据库获取 try {
const data = await this.db.get('cache', key);
if (data) {
this.addToCache(key, data);
return data.value;
}
} catch (error) {
console.error('从数据库获取缓存失败:', error);
}

return null;
}

async set(key, value, ttl = 3600000) { // 默认1小时 TTL
const cacheEntry = {
value,
expiresAt: Date.now() + ttl,
timestamp: Date.now()
};

// 存储到数据库 await this.db.put('cache', {
id: key,
...cacheEntry
});

// 添加到内存缓存 this.addToCache(key, cacheEntry);
}

addToCache(key, value) {
if (this.cache.size >= this.maxSize) {
// LRU 淘汰策略 const oldestKey = this.accessOrder.shift();
this.cache.delete(oldestKey);
}

this.cache.set(key, value);
this.updateAccessOrder(key);
}

updateAccessOrder(key) {
// 移除现有位置 const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
// 添加到末尾 this.accessOrder.push(key);
}

// 定期清理过期缓存 async cleanupExpired() {
const currentTime = Date.now();
const expiredKeys = [];

// 获取所有缓存条目 const allEntries = await this.db.getAll('cache');

for (const entry of allEntries) {
if (entry.expiresAt < currentTime) {
expiredKeys.push(entry.id);
}
}

// 删除过期条目 for (const key of expiredKeys) {
this.cache.delete(key);
await this.db.delete('cache', key);
}

return expiredKeys.length;
}
}

性能监控和分析

存储性能监控

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
class StoragePerformanceMonitor {
constructor() {
this.metrics = {
readTime: [],
writeTime: [],
operations: 0,
errors: 0
};
}

async measureOperation(operationName, operation) {
const startTime = performance.now();
let success = true;

try {
const result = await operation();
return result;
} catch (error) {
this.metrics.errors++;
success = false;
throw error;
} finally {
const endTime = performance.now();
const duration = endTime - startTime;

if (operationName.includes('read')) {
this.metrics.readTime.push(duration);
} else if (operationName.includes('write')) {
this.metrics.writeTime.push(duration);
}

this.metrics.operations++;

// 输出性能指标(可根据需要调整)
if (this.metrics.operations % 100 === 0) {
this.reportMetrics();
}
}
}

reportMetrics() {
const avgReadTime = this.metrics.readTime.reduce((a, b) => a + b, 0) / this.metrics.readTime.length;
const avgWriteTime = this.metrics.writeTime.reduce((a, b) => a + b, 0) / this.metrics.writeTime.length;

console.log(`性能报告:
平均读取时间: ${avgReadTime.toFixed(2)}ms 平均写入时间: ${avgWriteTime.toFixed(2)}ms 总操作数: ${this.metrics.operations}
错误数: ${this.metrics.errors}
`);
}

getPerformanceStats() {
return {
avgReadTime: this.metrics.readTime.length > 0
? this.metrics.readTime.reduce((a, b) => a + b, 0) / this.metrics.readTime.length
: 0,
avgWriteTime: this.metrics.writeTime.length > 0
? this.metrics.writeTime.reduce((a, b) => a + b, 0) / this.metrics.writeTime.length
: 0,
operationCount: this.metrics.operations,
errorRate: this.metrics.operations > 0
? (this.metrics.errors / this.metrics.operations) * 100
: 0
};
}
}

// 使用性能监控 const monitor = new StoragePerformanceMonitor();

async function monitoredStorageOperation(db, key, value) {
return await monitor.measureOperation('write', async () => {
return await db.put('data', { id: key, value });
});
}

存储空间监控

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
class StorageQuotaMonitor {
async getStorageInfo() {
if ('navigator' in window && 'storage' in navigator) {
try {
const estimate = await navigator.storage.estimate();
return {
quota: estimate.quota,
usage: estimate.usage,
usageDetails: estimate.usageDetails,
percentageUsed: ((estimate.usage / estimate.quota) * 100).toFixed(2)
};
} catch (error) {
console.warn('无法获取存储估计:', error);
return null;
}
}
return null;
}

async checkStorageThreshold(threshold = 0.8) {
const info = await this.getStorageInfo();
if (info) {
return info.usage / info.quota > threshold;
}
return false;
}

async requestPersistentStorage() {
if ('navigator' in window && 'storage' in navigator) {
try {
const isPersistent = await navigator.storage.persisted();
if (!isPersistent) {
const granted = await navigator.storage.persist();
return granted;
}
return true;
} catch (error) {
console.warn('持久化存储请求失败:', error);
return false;
}
}
return false;
}

formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}

// 使用示例 async function checkStorage() {
const monitor = new StorageQuotaMonitor();
const info = await monitor.getStorageInfo();

if (info) {
console.log(`存储使用情况:
已用: ${monitor.formatBytes(info.usage)}
总配额: ${monitor.formatBytes(info.quota)}
使用率: ${info.percentageUsed}%
`);

if (await monitor.checkStorageThreshold(0.8)) {
console.warn('存储空间即将不足,考虑清理旧数据');
}
}
}

实际应用场景

离线数据同步

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
class OfflineSyncManager {
constructor(localDb, remoteApi) {
this.localDb = localDb;
this.remoteApi = remoteApi;
this.syncQueue = [];
}

// 添加待同步数据 async addToSyncQueue(operation, data, timestamp = Date.now()) {
await this.localDb.add('sync_queue', {
operation, // 'create', 'update', 'delete'
data,
timestamp,
synced: false,
attempts: 0
});
}

// 执行同步 async sync() {
const unsyncedItems = await this.localDb.getAll('sync_queue', IDBKeyRange.only(false));

for (const item of unsyncedItems) {
try {
let result;

switch (item.operation) {
case 'create':
result = await this.remoteApi.create(item.data);
break;
case 'update':
result = await this.remoteApi.update(item.data.id, item.data);
break;
case 'delete':
result = await this.remoteApi.delete(item.data.id);
break;
}

// 同步成功,更新本地记录 await this.localDb.put('sync_queue', {
...item,
synced: true,
syncedAt: Date.now(),
serverResponse: result
});

} catch (error) {
// 更新重试次数 await this.localDb.put('sync_queue', {
...item,
attempts: item.attempts + 1,
lastError: error.message,
lastAttempt: Date.now()
});

// 如果重试次数过多,可能需要特殊处理 if (item.attempts >= 5) {
console.error('同步失败次数过多,需要人工干预:', item);
}
}
}
}

// 检查网络状态并自动同步 async autoSyncWhenOnline() {
if (navigator.onLine) {
await this.sync();
} else {
// 监听网络连接状态变化 window.addEventListener('online', () => {
this.sync().catch(console.error);
});
}
}
}

大文件存储和分片上传

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
93
94
95
96
class LargeFileStorage {
constructor(db, chunkSize = 1024 * 1024) { // 1MB chunks
this.db = db;
this.chunkSize = chunkSize;
}

// 分片上传文件 async uploadFile(file) {
const fileId = this.generateId();
const totalChunks = Math.ceil(file.size / this.chunkSize);

// 创建文件元数据 await this.db.put('files_metadata', {
id: fileId,
name: file.name,
size: file.size,
type: file.type,
totalChunks,
uploadedChunks: 0,
status: 'uploading',
uploadTime: Date.now()
});

// 逐块上传 for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, file.size);
const chunk = file.slice(start, end);

// 转换为 ArrayBuffer
const arrayBuffer = await chunk.arrayBuffer();

await this.db.put('file_chunks', {
id: `${fileId}_${i}`,
fileId,
chunkIndex: i,
data: arrayBuffer,
size: arrayBuffer.byteLength
});

// 更新上传进度 await this.updateUploadProgress(fileId, i + 1);
}

// 标记文件上传完成 await this.markUploadComplete(fileId);
}

// 合并文件 async getFile(fileId) {
const metadata = await this.db.get('files_metadata', fileId);
if (!metadata || metadata.status !== 'complete') {
throw new Error('文件不可用');
}

const chunks = [];
for (let i = 0; i < metadata.totalChunks; i++) {
const chunk = await this.db.get('file_chunks', `${fileId}_${i}`);
if (chunk) {
chunks.push(new Uint8Array(chunk.data));
}
}

// 合并所有块 const combinedBuffer = this.concatenateBuffers(chunks);
return new Blob([combinedBuffer], { type: metadata.type });
}

async updateUploadProgress(fileId, uploadedChunks) {
const metadata = await this.db.get('files_metadata', fileId);
await this.db.put('files_metadata', {
...metadata,
uploadedChunks,
progress: (uploadedChunks / metadata.totalChunks) * 100
});
}

async markUploadComplete(fileId) {
const metadata = await this.db.get('files_metadata', fileId);
await this.db.put('files_metadata', {
...metadata,
status: 'complete',
completeTime: Date.now()
});
}

generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

concatenateBuffers(buffers) {
const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;

for (const buffer of buffers) {
result.set(buffer, offset);
offset += buffer.length;
}

return result;
}
}

最佳实践总结

1. 存储方案选择指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 根据数据特征选择存储方案的决策树 function selectStorageStrategy(dataSize, dataType, accessPattern, persistenceNeeds) {
if (dataSize < 4000 && persistenceNeeds === 'session') {
return 'SessionStorage';
}

if (dataSize < 4000 && persistenceNeeds === 'permanent') {
return 'LocalStorage'; // 注意同步阻塞问题
}

if (dataSize < 100000 && accessPattern === 'simple') {
return 'LocalStorage with worker thread';
}

if (dataSize > 100000 || accessPattern === 'complex' || dataType === 'structured') {
return 'IndexedDB';
}

if (dataType === 'binary' && size > 1000000) {
return 'IndexedDB with streaming';
}

return 'IndexedDB';
}

2. 性能优化清单

  • 使用异步存储方案(IndexedDB)处理大数据
  • 实现数据分片以避免单次操作过大
  • 使用事务批处理提高写入性能
  • 实施适当的缓存策略减少重复读取
  • 监控存储配额防止超出限制
  • 实现错误处理和重试机制
  • 定期清理过期或无用数据

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
class ResilientStorage {
constructor(db) {
this.db = db;
this.backupStrategies = [];
}

async robustWrite(storeName, key, data) {
try {
return await this.db.put(storeName, { id: key, data, timestamp: Date.now() });
} catch (error) {
console.error('主存储写入失败:', error);

// 尝试备份策略 for (const backup of this.backupStrategies) {
try {
return await backup.write(storeName, key, data);
} catch (backupError) {
console.error('备份存储也失败:', backupError);
}
}

throw error;
}
}

async robustRead(storeName, key) {
try {
return await this.db.get(storeName, key);
} catch (error) {
console.error('主存储读取失败:', error);

// 尝试从备份恢复 for (const backup of this.backupStrategies) {
try {
const backupData = await backup.read(storeName, key);
if (backupData) {
// 将数据恢复到主存储 await this.db.put(storeName, backupData);
return backupData;
}
} catch (backupError) {
console.error('备份存储读取也失败:', backupError);
}
}

throw error;
}
}
}

总结

  • LocalStorage 适用于小量数据存储,但会阻塞主线程
  • IndexedDB 是大数据存储的首选方案,支持事务和复杂查询
  • 数据分片和压缩可以显著提升存储效率
  • 合理的缓存策略能大幅改善应用性能
  • 监控和错误处理是保证数据可靠性的关键
  • 根据具体需求选择合适的存储方案组合

最近在优化公司的离线应用,存储大数据的问题一度让我抓狂。经过一番折腾,终于找到了合适的方案组合。现在的应用即使在网络不佳的情况下也能流畅运行,用户体验提升了不少。看来技术选型真的很重要,盲目追求新技术不一定是最优解。

扩展阅读

  • MDN IndexedDB Guide
  • Web Storage Best Practices
  • Offline Web Applications
  • Progressive Web Apps Storage
  • Browser Storage Limits and Eviction Policies

参考资料

  • IndexedDB Specification: https://www.w3.org/TR/IndexedDB/
  • Web Storage API: https://Html.spec.whatwg.org/multipage/webstorage.Html
  • Cache API: https://w3c.github.io/ServiceWorker/specs/cache-API/
  • Service Worker: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
bulb