Category: guide
Browser Storage — localStorage, sessionStorage, IndexedDB
เปรียบเทียบและใช้งาน browser storage: localStorage สำหรับ simple KV, IndexedDB สำหรับ structured data ขนาดใหญ่
สารบัญ
เปรียบเทียบ Storage APIs
| localStorage | sessionStorage | IndexedDB | Cookies | |
|---|---|---|---|---|
| Capacity | ~5–10 MB | ~5 MB | ≥50 MB | ~4 KB |
| Persistence | ถาวร | ปิด tab = ลบ | ถาวร | ตาม expires |
| Type | string only | string only | structured data | string |
| Async | ❌ sync (blocking) | ❌ sync | ✓ async | ❌ sync |
| Web Workers | ❌ | ❌ | ✓ | ❌ |
| Scope | same origin | same tab | same origin | configurable |
localStorage — Simple Key-Value
// เก็บ
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));
// อ่าน
const theme = localStorage.getItem('theme'); // 'dark' | null
const user = JSON.parse(localStorage.getItem('user') ?? 'null');
// ลบ
localStorage.removeItem('theme');
localStorage.clear(); // ลบทั้งหมด
// ดูทั้งหมด
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key!);
}
// listen for changes จาก tab อื่น (ไม่ fire ใน tab ที่เปลี่ยน)
window.addEventListener('storage', (event) => {
console.log(event.key, event.oldValue, event.newValue);
});
Typed localStorage Wrapper
function createStorage<T extends Record<string, unknown>>(prefix: string) {
return {
get<K extends keyof T>(key: K): T[K] | null {
const raw = localStorage.getItem(`${prefix}:${String(key)}`);
if (raw === null) return null;
return JSON.parse(raw) as T[K];
},
set<K extends keyof T>(key: K, value: T[K]): void {
localStorage.setItem(`${prefix}:${String(key)}`, JSON.stringify(value));
},
remove<K extends keyof T>(key: K): void {
localStorage.removeItem(`${prefix}:${String(key)}`);
},
};
}
// ใช้งาน
const store = createStorage<{
theme: 'light' | 'dark';
fontSize: number;
user: { id: string; name: string };
}>('myapp');
store.set('theme', 'dark'); // ✓
store.set('theme', 'blurple'); // ❌ TypeScript error
store.get('theme'); // 'light' | 'dark' | null
IndexedDB — Structured Data ขนาดใหญ่
IndexedDB ซับซ้อนกว่า localStorage แต่จำเป็นเมื่อ:
- ข้อมูลมากกว่า 5MB
- ต้องการ query / filter
- ต้องการ indexes
- ต้องการทำงานใน Web Worker
ใช้ผ่าน idb library (Wrapper ที่ดีที่สุด)
npm install idb
import { openDB, type DBSchema } from 'idb';
interface AppDB extends DBSchema {
notes: {
key: string;
value: { id: string; title: string; content: string; createdAt: number };
indexes: { 'by-date': number };
};
settings: {
key: string;
value: unknown;
};
}
// เปิด database (สร้างถ้าไม่มี)
const db = await openDB<AppDB>('my-app', 1, {
upgrade(db, oldVersion) {
if (oldVersion < 1) {
const noteStore = db.createObjectStore('notes', { keyPath: 'id' });
noteStore.createIndex('by-date', 'createdAt');
db.createObjectStore('settings');
}
},
});
// CRUD
await db.put('notes', {
id: crypto.randomUUID(),
title: 'Meeting notes',
content: '...',
createdAt: Date.now(),
});
const note = await db.get('notes', 'some-id');
// Query ด้วย index
const allNotes = await db.getAllFromIndex('notes', 'by-date');
// Delete
await db.delete('notes', 'some-id');
// Count
const count = await db.count('notes');
Raw IndexedDB API (ไม่ใช้ library)
function openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDB', 1);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('items')) {
db.createObjectStore('items', { keyPath: 'id', autoIncrement: true });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function addItem(db: IDBDatabase, data: object): Promise<number> {
return new Promise((resolve, reject) => {
const tx = db.transaction('items', 'readwrite');
const store = tx.objectStore('items');
const request = store.add(data);
request.onsuccess = () => resolve(request.result as number);
request.onerror = () => reject(request.error);
});
}
Offline-first Pattern
// Cache API สำหรับ network resources (ใช้ใน Service Worker)
const cache = await caches.open('v1');
await cache.add('/api/data');
const response = await cache.match('/api/data');
// Pattern: Cache first, Network fallback
async function fetchWithCache(url: string): Promise<Response> {
const cached = await caches.match(url);
if (cached) return cached;
const response = await fetch(url);
const cache = await caches.open('v1');
cache.put(url, response.clone());
return response;
}
// Pattern: Network first, Cache fallback
async function fetchFreshFirst(url: string): Promise<Response> {
try {
const response = await fetch(url);
const cache = await caches.open('v1');
cache.put(url, response.clone());
return response;
} catch {
const cached = await caches.match(url);
if (cached) return cached;
throw new Error('Offline and no cache available');
}
}
Storage Quota
// ตรวจสอบ storage quota
if ('storage' in navigator && 'estimate' in navigator.storage) {
const { usage, quota } = await navigator.storage.estimate();
const usageMB = (usage ?? 0) / 1024 / 1024;
const quotaMB = (quota ?? 0) / 1024 / 1024;
console.log(`Used: ${usageMB.toFixed(1)}MB / ${quotaMB.toFixed(0)}MB`);
}
// ขอ persistent storage (ไม่ถูก evict โดย browser)
const isPersistent = await navigator.storage.persist();
console.log(isPersistent ? 'Storage will persist' : 'Storage may be cleared');
เลือกใช้อะไร
ข้อมูลเล็ก (<100KB) + อ่านเขียนบ่อย + ต้องอยู่ข้าม tabs
→ localStorage
ข้อมูลชั่วคราวใน session เดียว
→ sessionStorage
ข้อมูลมาก / ต้องการ query / offline-first
→ IndexedDB (ใช้ผ่าน idb library)
API responses ที่ cache ได้ (network layer)
→ Cache API (ใน Service Worker)
Auth tokens, session cookies
→ httpOnly cookies (ไม่ใช้ localStorage — XSS เข้าถึงได้)