1. Pengenalan Web Storage
Browser modern menyediakan beberapa mekanisme untuk menyimpan data di sisi client (di dalam browser pengguna). Ini sangat berguna untuk menyimpan preferensi user, data cache, state aplikasi, dan bahkan data offline.
Perbandingan Semua Storage
| Fitur | localStorage | sessionStorage | IndexedDB | Cookies |
|---|---|---|---|---|
| Kapasitas | ~5-10 MB | ~5-10 MB | Ratusan MB+ | ~4 KB |
| Tipe Data | String | String | Bebas (object) | String |
| Persistence | Selamanya | Per tab session | Selamanya | Bisa expiry |
| Dikirim ke Server | Tidak | Tidak | Tidak | Ya (setiap request) |
| Async? | Sinkron | Sinkron | Async | Sinkron |
| Complex Query | ❌ | ❌ | ✅ | ❌ |
| Web Workers | ❌ | ❌ | ✅ | ❌ |
Perlu menyimpan data di browser?
│
├── Data kecil (preferences, settings)?
│ └── localStorage
│
├── Data sementara (form draft)?
│ └── sessionStorage
│
├── Data besar/terstruktur (offline DB)?
│ └── IndexedDB
│
├── Perlu dikirim ke server?
│ └── Cookies
│
└── Perlu cache HTTP response?
└── Cache API (via Service Worker)
2. localStorage
localStorage adalah penyimpanan key-value yang sederhana dan persisten. Data disimpan selamanya (sampai dihapus manual) dan tersedia untuk semua tab dari origin yang sama.
API Dasar localStorage
// ===== API Dasar localStorage =====
// Menyimpan data (hanya STRING!)
localStorage.setItem('nama', 'Beebane');
localStorage.setItem('umur', '25'); // ← angka jadi string!
localStorage.setItem('aktif', 'true'); // ← boolean jadi string!
// Membaca data (selalu mengembalikan string atau null)
const nama = localStorage.getItem('nama'); // → 'Beebane'
const umur = localStorage.getItem('umur'); // → '25' (bukan 25!)
const tidakAda = localStorage.getItem('x'); // → null
// Menghapus data
localStorage.removeItem('umur');
// Menghapus SEMUA data
localStorage.clear();
// Menghitung jumlah item
console.log(localStorage.length); // → 1 (setelah hapus 'umur')
// Akses berdasarkan index
const key = localStorage.key(0); // → 'nama'
// ===== Shortcut: Property Access =====
localStorage.tema = 'dark'; // Sama dengan setItem
console.log(localStorage.tema); // Sama dengan getItem
delete localStorage.tema; // Sama dengan removeItem
Menyimpan Object & Array
// ===== Menyimpan Object/Array (gunakan JSON) =====
// localStorage hanya bisa string → gunakan JSON.stringify/parse
const pengaturan = {
tema: 'dark',
bahasa: 'id',
fontSize: 16,
notifikasi: true,
sidebar: {
collapsed: false,
width: 280
}
};
// Simpan
localStorage.setItem('pengaturan', JSON.stringify(pengaturan));
// Baca
const saved = JSON.parse(localStorage.getItem('pengaturan'));
console.log(saved.tema); // → 'dark'
console.log(saved.sidebar.width); // → 280
// ===== Menyimpan Array =====
const favorites = ['JavaScript', 'Python', 'Rust'];
localStorage.setItem('favorites', JSON.stringify(favorites));
const fav = JSON.parse(localStorage.getItem('favorites'));
console.log(fav.includes('Python')); // → true
// ===== Wrapper Functions (aman dari error) =====
function setItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
// QuotaExceededError — storage penuh!
console.error('Gagal menyimpan:', error.message);
return false;
}
}
function getItem(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('Gagal membaca:', error.message);
return defaultValue;
}
}
function removeItem(key) {
localStorage.removeItem(key);
}
// Penggunaan:
setItem('user', { nama: 'Beebane', role: 'admin' });
const user = getItem('user', { nama: 'Guest', role: 'guest' });
console.log(user.nama); // → 'Beebane'
JANGAN menyimpan data sensitif di localStorage (token autentikasi, password, data pribadi). localStorage bisa diakses oleh semua JavaScript di halaman — jika ada XSS, data Anda bisa dicuri. Gunakan HttpOnly Cookies untuk data sensitif.
3. sessionStorage
sessionStorage memiliki API yang identik dengan localStorage, tapi datanya hilang saat tab ditutup. Setiap tab memiliki sessionStorage yang terpisah.
// ===== sessionStorage: API Identik dengan localStorage =====
// Simpan
sessionStorage.setItem('formData', JSON.stringify({
nama: 'Beebane',
email: 'bee@example.com',
pesan: 'Halo dunia!'
}));
// Baca
const form = JSON.parse(sessionStorage.getItem('formData'));
console.log(form.nama); // → 'Beebane'
// Hapus
sessionStorage.removeItem('formData');
// Clear semua
sessionStorage.clear();
// ===== Perbedaan dengan localStorage =====
// 1. Data hilang saat tab ditutup
// localStorage: data tetap ada
// sessionStorage: data hilang
// 2. Terpisah per tab
// localStorage: dibagi semua tab dari origin yang sama
// sessionStorage: setiap tab punya sendiri
// 3. Tidak terpengaruh window.open()
// sessionStorage baru dibuat untuk window.open() dengan '_blank'
// ===== Contoh: Draft Form =====
// Simpan draft otomatis agar tidak hilang saat refresh
const formInput = document.getElementById('message');
// Load draft saat halaman dibuka
const draft = sessionStorage.getItem('draft-message');
if (draft) {
formInput.value = draft;
}
// Auto-save draft saat user mengetik
formInput.addEventListener('input', () => {
sessionStorage.setItem('draft-message', formInput.value);
});
// Hapus draft saat form berhasil dikirim
form.addEventListener('submit', () => {
sessionStorage.removeItem('draft-message');
});
// ===== Contoh: Halaman State =====
// Menyimpan scroll position, tab aktif, dll
function savePageState() {
const state = {
scrollY: window.scrollY,
activeTab: document.querySelector('.tab.active')?.dataset.tab,
filter: document.getElementById('filter')?.value,
sort: document.getElementById('sort')?.value
};
sessionStorage.setItem('pageState', JSON.stringify(state));
}
function restorePageState() {
const saved = JSON.parse(sessionStorage.getItem('pageState'));
if (saved) {
window.scrollTo(0, saved.scrollY);
if (saved.activeTab) {
document.querySelector(`[data-tab="${saved.activeTab}"]`)?.click();
}
}
}
// Auto-save sebelum unload
window.addEventListener('beforeunload', savePageState);
// Restore saat halaman dimuat
window.addEventListener('load', restorePageState);
4. Storage Events
Event storage di-trigger di tab lain (bukan tab yang mengubah) saat localStorage berubah. Ini memungkinkan sinkronisasi antar tab.
// ===== Storage Event =====
// Event ini HANYA di-trigger di TAB LAIN!
// Tidak di-trigger di tab yang mengubah storage.
window.addEventListener('storage', (event) => {
console.log('Storage berubah!');
console.log(' Key:', event.key); // Key yang berubah
console.log(' Old:', event.oldValue); // Nilai lama
console.log(' New:', event.newValue); // Nilai baru
console.log(' URL:', event.url); // URL halaman yang mengubah
console.log(' StorageArea:', event.storageArea); // localStorage object
});
// ===== Contoh: Auto-Logout di Semua Tab =====
// Tab 1: User logout → set flag
function logout() {
localStorage.setItem('logout-event', Date.now().toString());
localStorage.removeItem('auth-token');
window.location.href = '/login';
}
// Tab 2, 3, dll: Listen untuk logout event
window.addEventListener('storage', (event) => {
if (event.key === 'logout-event') {
// User logout di tab lain → redirect ke login
window.location.href = '/login';
}
});
// ===== Contoh: Theme Sync antar Tab =====
// Tab 1: Ganti tema
function changeTheme(theme) {
localStorage.setItem('theme', theme);
applyTheme(theme); // Langsung terapkan di tab ini
}
// Tab 2, 3, dll: Ikut ganti tema
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
applyTheme(event.newValue);
}
});
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
5. Cookies vs Storage
// ===== Cookies =====
// Cookies dikirim ke server SETIAP request (overhead!)
// Kapasitas: ~4KB per cookie
// Set cookie
document.cookie = "username=Beebane; expires=Fri, 31 Dec 2026 23:59:59 GMT; path=/";
document.cookie = "theme=dark; max-age=86400; path=/"; // 1 hari
document.cookie = "session=abc123; path=/; SameSite=Strict; Secure";
// Set cookie dengan JavaScript Date
const expiry = new Date();
expiry.setDate(expiry.getDate() + 7); // 7 hari
document.cookie = `token=xyz; expires=${expiry.toUTCString()}; path=/`;
// Baca cookies (satu string besar!)
console.log(document.cookie);
// → "username=Beebane; theme=dark; session=abc123; token=xyz"
// Parse cookies
function getCookie(name) {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === name) return decodeURIComponent(value);
}
return null;
}
console.log(getCookie('username')); // → 'Beebane'
// Hapus cookie (set expires ke masa lalu)
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
// ===== Cookie Attributes =====
// path=/ → Berlaku untuk semua path
// domain=.example.com → Berlaku untuk subdomain
// max-age=3600 → Berlaku 3600 detik
// expires=DATE → Sampai tanggal tertentu
// secure → Hanya dikirim via HTTPS
// HttpOnly → Tidak bisa diakses JavaScript (server only!)
// SameSite=Lax → CSRF protection (default modern browsers)
// SameSite=Strict → Cookie hanya untuk same-site request
// ===== Perbandingan: Kapan Menggunakan Cookie? =====
// Cookie: Autentikasi, CSRF token, data yang perlu dikirim ke server
// localStorage: Preferences, cache, data yang TIDAK perlu ke server
// sessionStorage: Draft form, state sementara per tab
| Skenario | Gunakan | Alasan |
|---|---|---|
| Token autentikasi | HttpOnly Cookie | Aman dari XSS, dikirim otomatis |
| User preferences | localStorage | Persisten, tidak perlu ke server |
| Form draft | sessionStorage | Hilang saat tab ditutup |
| Shopping cart | localStorage / IndexedDB | Persisten, bisa besar |
| API response cache | IndexedDB / Cache API | Data besar, struktur kompleks |
| Theme setting | localStorage | Persisten, kecil |
6. IndexedDB Dasar
IndexedDB adalah database bawaan browser yang powerful — bisa menyimpan data dalam jumlah besar, mendukung indexing, transaksi, dan query kompleks. Ini adalah solusi terbaik untuk penyimpanan data offline yang besar dan terstruktur.
// ===== Membuka Database =====
// IndexedDB bersifat async dan berbasis event/callback
function openDatabase() {
return new Promise((resolve, reject) => {
// Buka atau buat database baru
const request = indexedDB.open('BeebaneDB', 1);
// Dipanggil saat database perlu di-upgrade (versi baru)
request.onupgradeneeded = (event) => {
const db = event.target.result;
console.log('Upgrading database...');
// Buat object store (mirip "tabel" di SQL)
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', {
keyPath: 'id', // Primary key
autoIncrement: true // Auto-increment ID
});
// Buat index untuk pencarian
store.createIndex('email', 'email', { unique: true });
store.createIndex('nama', 'nama', { unique: false });
store.createIndex('role', 'role', { unique: false });
}
if (!db.objectStoreNames.contains('posts')) {
const postStore = db.createObjectStore('posts', {
keyPath: 'id',
autoIncrement: true
});
postStore.createIndex('authorId', 'authorId');
postStore.createIndex('createdAt', 'createdAt');
}
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('Database berhasil dibuka!');
resolve(db);
};
request.onerror = (event) => {
console.error('Gagal membuka database:', event.target.error);
reject(event.target.error);
};
});
}
// ===== CREATE: Menambah Data =====
async function addUser(user) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
// Mulai transaksi (readwrite)
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const request = store.add(user);
request.onsuccess = () => {
console.log('User ditambahkan, ID:', request.result);
resolve(request.result); // ID yang di-generate
};
request.onerror = () => {
console.error('Gagal menambah user:', request.error);
reject(request.error);
};
});
}
// Penggunaan:
const newId = await addUser({
nama: 'Beebane',
email: 'bee@example.com',
role: 'admin',
createdAt: new Date()
});
// ===== READ: Membaca Data =====
async function getUserById(id) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Membaca semua data
async function getAllUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// ===== UPDATE: Memperbarui Data =====
async function updateUser(user) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const request = store.put(user); // put = insert atau update
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// ===== DELETE: Menghapus Data =====
async function deleteUser(id) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve(true);
request.onerror = () => reject(request.error);
});
}
7. IndexedDB Lanjutan
Menggunakan Index
// ===== Query menggunakan Index =====
async function getUserByEmail(email) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const index = store.index('email'); // Gunakan index 'email'
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Cari semua admin
async function getUsersByRole(role) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const index = store.index('role');
const request = index.getAll(role); // Filter by role
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// ===== Cursor: Iterasi Data =====
async function iterateUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const results = [];
// Buka cursor untuk iterasi
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// Ada data
results.push(cursor.value);
cursor.continue(); // Lanjut ke data berikutnya
} else {
// Iterasi selesai
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
// ===== Range Query =====
async function getUsersInRange(startId, endId) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
// IDBKeyRange untuk range query
const range = IDBKeyRange.bound(startId, endId);
const request = store.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Range yang lebih fleksibel:
const lowerBound = IDBKeyRange.lowerBound(5); // id >= 5
const upperBound = IDBKeyRange.upperBound(10); // id <= 10
const only5 = IDBKeyRange.only(5); // id === 5
const exclude5 = IDBKeyRange.lowerBound(5, true); // id > 5 (exclude 5)
Transaksi & Batch Operations
// ===== Transaksi: Atomic Operations =====
// Transaksi memastikan semua operasi berhasil atau semua gagal
async function transferData(fromId, toId, amount) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
// Satu transaksi untuk kedua operasi
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const getFrom = store.get(fromId);
const getTo = store.get(toId);
getFrom.onsuccess = () => {
getTo.onsuccess = () => {
const from = getFrom.result;
const to = getTo.result;
if (!from || !to) {
tx.abort(); // Batalkan semua!
reject(new Error('User tidak ditemukan'));
return;
}
from.saldo -= amount;
to.saldo += amount;
store.put(from);
store.put(to);
};
};
// Transaksi selesai (semua operasi berhasil)
tx.oncomplete = () => {
console.log('Transfer berhasil!');
resolve(true);
};
// Transaksi gagal (salah satu operasi error)
tx.onerror = () => {
console.error('Transfer gagal:', tx.error);
reject(tx.error);
};
});
}
// ===== Batch Insert =====
async function batchInsert(storeName, items) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
items.forEach(item => store.add(item));
tx.oncomplete = () => {
console.log(`${items.length} item ditambahkan`);
resolve(true);
};
tx.onerror = () => reject(tx.error);
});
}
// ===== Count & Clear =====
async function countUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const request = store.count();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function clearAllUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const request = store.clear();
request.onsuccess = () => resolve(true);
request.onerror = () => reject(request.error);
});
}
8. Storage Wrapper Library
// ===== Storage Wrapper yang Clean =====
class Storage {
constructor(storage = localStorage) {
this.storage = storage;
}
set(key, value, options = {}) {
try {
const data = {
value,
timestamp: Date.now(),
expiry: options.expiry
? Date.now() + options.expiry
: null
};
this.storage.setItem(key, JSON.stringify(data));
return true;
} catch (error) {
console.error('Storage set error:', error);
return false;
}
}
get(key, defaultValue = null) {
try {
const raw = this.storage.getItem(key);
if (!raw) return defaultValue;
const data = JSON.parse(raw);
// Cek expiry
if (data.expiry && Date.now() > data.expiry) {
this.storage.removeItem(key);
return defaultValue;
}
return data.value;
} catch (error) {
console.error('Storage get error:', error);
return defaultValue;
}
}
remove(key) {
this.storage.removeItem(key);
}
has(key) {
return this.get(key) !== null;
}
clear() {
this.storage.clear();
}
keys() {
return Object.keys(this.storage);
}
// Tambah item ke array yang tersimpan
push(key, item) {
const arr = this.get(key, []);
arr.push(item);
return this.set(key, arr);
}
// Hapus item dari array
removeItem(key, predicate) {
const arr = this.get(key, []);
const filtered = arr.filter(item => !predicate(item));
return this.set(key, filtered);
}
// Increment counter
increment(key, amount = 1) {
const current = this.get(key, 0);
return this.set(key, current + amount);
}
}
// Penggunaan:
const store = new Storage(localStorage);
const session = new Storage(sessionStorage);
// Simpan dengan expiry (1 jam)
store.set('token', 'abc123', { expiry: 60 * 60 * 1000 });
// Baca (null jika expired)
const token = store.get('token');
// Array operations
store.push('favorites', 'JavaScript');
store.push('favorites', 'Python');
const favs = store.get('favorites'); // ['JavaScript', 'Python']
// Counter
store.increment('pageViews');
store.increment('pageViews');
console.log(store.get('pageViews')); // 2
9. Best Practices
Handling QuotaExceededError
// ===== Cek Storage yang Tersedia =====
async function checkStorageQuota() {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
const usedMB = (estimate.usage / 1024 / 1024).toFixed(2);
const quotaMB = (estimate.quota / 1024 / 1024).toFixed(2);
const percent = ((estimate.usage / estimate.quota) * 100).toFixed(1);
console.log(`Storage terpakai: ${usedMB} MB dari ${quotaMB} MB (${percent}%)`);
return { used: usedMB, quota: quotaMB, percent };
}
console.log('Storage estimate API tidak tersedia');
return null;
}
// ===== Tangani QuotaExceededError =====
function safeSetItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
if (error.name === 'QuotaExceededError' ||
error.code === 22 ||
error.code === 1014) {
console.error('Storage penuh! Membersihkan data lama...');
cleanupOldData();
// Coba lagi
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e2) {
console.error('Masih gagal setelah cleanup');
// Fallback: gunakan IndexedDB
saveToIndexedDB(key, value);
}
}
}
}
function cleanupOldData() {
// Hapus item yang sudah expired atau tidak penting
const keys = Object.keys(localStorage);
for (const key of keys) {
try {
const data = JSON.parse(localStorage.getItem(key));
if (data && data.expiry && Date.now() > data.expiry) {
localStorage.removeItem(key);
console.log('Menghapus expired:', key);
}
} catch {
// Bukan format JSON → skip
}
}
}
// ===== Minta Persistent Storage =====
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persisted();
if (isPersisted) {
console.log('Storage sudah persistent');
return true;
}
const granted = await navigator.storage.persist();
console.log(granted
? '✅ Persistent storage diizinkan!'
: '❌ Persistent storage ditolak'
);
return granted;
}
return false;
}
Checklist Best Practices
| Praktik | Penjelasan |
|---|---|
| JSON.stringify/parse | Selalu gunakan untuk localStorage/sessionStorage |
| Error handling | Selalu tangkap QuotaExceededError |
| Jangan simpan sensitif | Token/password jangan di localStorage |
| Namespacing | Prefix key: app_userName, app_theme |
| Expiry system | Simpan timestamp dan cek sebelum baca |
| IndexedDB untuk besar | Lebih dari 1MB → gunakan IndexedDB |
| Persistent storage | Minta persistent agar tidak dihapus browser |
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut: