How Browsers Work · Part 10 — Storage & the Platform
Where the web stores data: cookie scope, Web Storage, IndexedDB, Cache API and service workers — sync/async trade-offs, quotas, partitioning and lifecycle.
Phần 8–9 đã đi qua bộ nhớ trong renderer và byte trên dây. Phần này về client storage — API app dùng để nhớ state qua reload/page session, hỗ trợ offline, và cache response lập trình được.
Web Storage, IndexedDB và Cache API chủ yếu dùng storage key dựa trên origin (scheme + host + port), và browser có thể partition thêm theo top-level site trong third-party context. Cookie là ngoại lệ quan trọng: matching dựa host/domain + path cùng cờ Secure/SameSite/partition; port không phải ranh giới cookie. Chọn sai API có thể chặn main thread, vượt quota, hoặc gửi dữ liệu thừa trên request.
1. Mô hình lưu trữ theo origin
Origin là bộ (scheme, hostname, port). localStorage, IndexedDB và Cache API mặc định theo origin; sessionStorage còn được partition theo top-level browsing context/page session. Storage Standard gọi primitive này là storage key/bucket và để quota là implementation-defined: WHATWG Storage.
https://shop.example.com:443 ← origin A
├─ localStorage (A)
├─ IndexedDB database "orders" (A)
├─ Cache API "v1-static" (A)
└─ (cookies shown separately: host/domain + path, not origin/port)
https://cdn.example.com:443 ← origin B (separate bucket)
└─ its own isolated storage
Trình duyệt còn partition state trong third-party context để giảm tracking, nhưng policy khác nhau. Với CHIPS, cookie có attribute Partitioned; Secure được double-key bởi cookie host và top-level site, nên widget nhúng trên shop.example không tự dùng cùng partition với widget trên news.example. Đây là opt-in cookie partitioning, không đồng nghĩa mọi browser đã xóa mọi third-party cookie. Xem CHIPS guide.
Hai tab cùng origin có thể chia sẻ
localStorage, IndexedDB và Cache API.sessionStoragekhông chia sẻ như vậy: mỗi top-level browsing context có page session riêng (tab mới từ opener có thể được clone ban đầu rồi tách). Dùng sự kiệnstoragehoặcBroadcastChannelkhi thật sự cần đồng bộ.
2. So sánh cơ chế lưu trữ
| Cơ chế | Gửi kèm HTTP? | Giới hạn | Kiểu API | Kiểu dữ liệu |
|---|---|---|---|---|
| Cookies | Yes — every matching request | ~4 KB per cookie | Set-Cookie; document.cookie sync cho cookie JS-visible | Chuỗi |
| sessionStorage | No | Browser quota; page session riêng | Sync | Chuỗi |
| localStorage | No | Thường ~5 MiB/origin; phải bắt QuotaExceededError | Sync | Chuỗi |
| IndexedDB | No | Quota implementation-defined | Async (transactions) | Structured-clone values |
| Cache API | No | Thường chung storage quota với IDB | Async | Request / Response pairs |
Bảng là cây quyết định trong một nhìn: cần token session server đọc được → cookie; pref UI nhỏ → localStorage; dataset offline lớn → IndexedDB; chặn fetch offline → Cache API + Service Worker.
3. Cookie: nhỏ, tự động, server thấy được
HTTP cookie là cặp tên-giá trị thường được server đặt qua Set-Cookie (cookie không-HttpOnly cũng có thể do JS đặt). Browser tự gắn cookie phù hợp vào request qua header Cookie; cạm bẫy lớn nhất chính là gửi tự động theo scope/policy.
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=604800
Thuộc tính quan trọng:
| Thuộc tính | Mục đích |
|---|---|
HttpOnly | JS không đọc — giảm đánh cắp token XSS |
Secure | Chỉ gửi qua HTTPS |
SameSite=Lax/Strict/None | Kiểm soát gửi cross-site (giảm CSRF) |
Max-Age / Expires | Thời hạn |
Domain / Path | Phạm vi matching request; Path không phải security boundary |
Partitioned | Double-key cookie theo top-level site; bắt buộc Secure |
Cookie nhỏ (browser phải hỗ trợ tối thiểu cỡ ~4 KiB/cookie, nhưng limit/count cụ thể khác nhau) và document.cookie là API chuỗi đồng bộ. Với session auth browser-based, cookie HttpOnly; Secure; SameSite thường giảm khả năng token bị đọc bởi XSS, nhưng vẫn cần CSRF/authz phù hợp. Tránh payload lớn vì cookie matching sẽ phình request.
4. Web Storage: localStorage và sessionStorage
Web Storage API có hai kho cùng origin:
localStorage— thường sống qua browser restart cho đến khi user/site/policy xóa; private browsing dùng lifetime tạm.sessionStorage— theo origin và top-level browsing context/page session; sống qua reload, thường kết thúc khi tab/window session đóng (session restore có thể phục hồi).
Cả hai đồng bộ và chỉ chuỗi:
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // synchronous API; main thread waits for the operation
// Objects must be serialized manually
localStorage.setItem('prefs', JSON.stringify({ density: 'compact' }));
Vì sao storage đồng bộ nguy hiểm trên luồng chính
Đọc/ghi localStorage chặn main thread ở API boundary; browser có thể dùng memory cache/IPC nên không phải mỗi call đều là disk I/O, nhưng serialization, cross-process coordination hay cold access vẫn có cost. Giữ giá trị nhỏ, không dùng Web Storage như database. Web Storage không có HttpOnly: script cùng origin — kể cả XSS — đọc được các value đó.
localStoragekhông chia sẻ với worker. Dữ liệu có cấu trúc lớn hơn dùng IndexedDB.
5. IndexedDB: database client-side thật sự
IndexedDB là object store giao dịch, request async, cho dữ liệu có cấu trúc quy mô lớn. Nó không giữ main thread chờ I/O theo API đồng bộ như Web Storage, nhưng callback, structured clone và xử lý result vẫn có thể tốn main-thread CPU. IDB hỗ trợ index, cursor, Blob/File, và schema migration theo version.
let dbPromise;
function openDb() {
dbPromise ??= new Promise((resolve, reject) => {
const req = indexedDB.open('app-db', 1);
req.onupgradeneeded = () => {
const db = req.result;
const store = db.createObjectStore('notes', { keyPath: 'id' });
store.createIndex('byUpdated', 'updatedAt');
};
req.onsuccess = () => {
const db = req.result;
db.onversionchange = () => db.close();
resolve(db);
};
req.onerror = () => reject(req.error);
});
return dbPromise;
}
async function saveNote(note) {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
tx.objectStore('notes').put(note);
tx.oncomplete = () => resolve(undefined);
tx.onerror = () => reject(tx.error);
});
}
Khái niệm cần nắm:
- Database → object store (như bảng) → record key theo key path hoặc out-of-line key.
- Transaction là đơn vị công việc — tự commit khi request hoàn tất và event loop không còn giữ transaction active;
awaitcông việc không liên quan ở giữa có thể khiến transaction đóng trước khi bạn enqueue request tiếp. - Version — tăng
versiontrongopen()và migrate trongonupgradeneeded.
Thư viện như idb hoặc Dexie có thể bọc API event-based. IndexedDB phù hợp cho structured client data/offline dataset lớn hơn Web Storage; dữ liệu user không thể thay thế vẫn cần sync/export/backup, vì client storage không phải durable database tuyệt đối.
6. Cache API và Service Worker
Cache API lưu cặp Request/Response — cùng hình dạng pipeline fetch dùng (Phần 9). Nó dùng được từ window và worker, thường kết hợp với Service Worker: worker global event-driven có thể bị terminate khi idle rồi khởi động lại cho event sau, không phải daemon sống mãi.
Lifecycle Service Worker
Register sw.js
│
▼
install → precache static shell (cache.addAll)
│
▼
activate → delete old caches (cache.delete)
│
▼
fetch → intercept network requests (event.respondWith)
// sw.js (simplified)
const CACHE_PREFIX = 'my-app-shell-';
const CACHE = `${CACHE_PREFIX}v1`;
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(['/', '/app.css', '/app.js']))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE)
.map((key) => caches.delete(key))
)
)
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches
.open(CACHE)
.then((cache) => cache.match(event.request))
.then((cached) => cached ?? fetch(event.request))
);
});
Đăng ký thành công chưa có nghĩa page hiện tại được control. Worker mới install rồi activate; theo lifecycle mặc định, page thường chỉ có controller sau navigation/reload kế tiếp. clients.claim() có thể nhận control sớm hơn, nhưng app phải sẵn sàng cho việc page đang chạy chuyển controller.
Chiến lược cache phổ biến
| Chiến lược | Hành vi | Phù hợp |
|---|---|---|
| Cache-first | Cache → network on miss | Asset tĩnh, app shell |
| Network-first | Network → cache fallback | Dữ liệu API cần tươi |
| Stale-while-revalidate | Return cache immediately, update in background | Nội dung bán tĩnh |
| Network-only | Bỏ qua Cache API; fetch() vẫn theo HTTP-cache semantics | Request cần network policy/server freshness |
Cạm bẫy: cache HTML hung hăng phục vụ phiên bản cũ; cache.put() tăng không giới hạn làm cạn quota; update worker mới ở trạng thái waiting trong khi tab cũ còn mở. skipWaiting()/clients.claim() không phải “luôn bật” — activate code mới ngay có thể trộn HTML/runtime cũ với asset mới. Version cache, chỉ xóa cache thuộc app của mình, và thiết kế update UX/atomic asset strategy rõ.
7. Quota, eviction, và persistence
Storage không vô hạn. Browser đặt quota implementation-defined cho storage bucket/origin; IndexedDB, Cache API và API khác thường tính vào cùng usage. estimate() cố ý là ước lượng, có thể gồm padding để giảm fingerprinting:
const { usage, quota } = await navigator.storage.estimate();
console.log(`Using ${usage} of ${quota} bytes`);
Hai chế độ persistence quan trọng:
- Best-effort (mặc định) — browser có thể evict theo policy khi storage pressure; thuật toán khác nhau theo engine.
- Persistent — yêu cầu qua
navigator.storage.persist(); Promise trả boolean, còn prompt/auto-grant/heuristic là policy của browser.
Chế độ duyệt riêng dùng profile tạm — giả định dữ liệu có thể mất. Không bao giờ coi client là nguồn sự thật duy nhất cho dữ liệu không thay thế được.
8. Privacy: partitioning và storage third-party
Unpartitioned third-party state cho phép theo dõi cross-site. Browser hiện áp dụng mức blocking/partition khác nhau theo engine và lựa chọn user:
- Storage Partitioning —
tracker.comtrong iframe trênshop.comcó bucket riêng vớitracker.comtrênnews.com. - CHIPS — cookie có
Partitionedđược double-key theo top-level site. - Storage Access API — embed có thể xin quyền truy cập unpartitioned cookie/state trong use case user-facing.
- Related Website Sets — cơ chế Chromium hỗ trợ một số flow liên-site liên quan qua Storage Access API; không phải ngoại lệ web-wide hay tên cũ “First-Party Sets”.
Với kỹ sư: auth và cá nhân hóa nên ở origin first-party; embed cross-site không nên dựa storage third-party dùng chung. Dùng cookie SameSite, redirect OAuth trên domain riêng, và postMessage cho giao tiếp cross-frame.
9. Khi nào dùng cái nào
| Nhu cầu | Dùng | Tránh |
|---|---|---|
| Session token for server auth | HttpOnly cookie | localStorage (XSS đọc được) |
| Theme / language preference | localStorage | Cookie (gửi mọi request) |
| Form draft for one tab session | sessionStorage | Cookies |
| Offline docs, 10k+ records, binary blobs | IndexedDB | localStorage (size + sync) |
| Offline shell + cached assets | Cache API + Service Worker | Manual IDB of Response bodies |
| ”Maybe next page” assets | HTTP prefetch (Part 9) | Cache API |
Nền tảng cho nhiều lớp — HTTP cache (tự động, Phần 9), Cache API (lập trình), IndexedDB (state app có cấu trúc) — xếp chồng có chủ đích, không cùng lúc cho cùng byte.
Tóm tắt
- Web Storage/IndexedDB/Cache API chủ yếu theo origin (có thể partition thêm); cookie theo host/domain + path và có thể double-key bằng
Partitioned. - Cookie nhỏ, tự gửi, server đọc được — dùng auth session với
HttpOnly/Secure/SameSite. - localStorage/sessionStorage đồng bộ và chỉ chuỗi — ổn pref nhỏ, nguy hiểm dữ liệu lớn hoặc đường nóng.
- IndexedDB async, lớn, có cấu trúc — database client cho app offline-first.
- Cache API + Service Worker cho chặn fetch lập trình và chiến lược offline.
- Theo dõi
navigator.storage.estimate(); yêu cầu storage persistent khi eviction hại UX.
Phần tiếp theo: Mô hình bảo mật — same-origin policy, CORS, CSP, và cách site isolation biến ranh giới tiến trình thành ranh giới bảo mật.