jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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. sessionStorage khô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ện storage hoặc BroadcastChannel khi 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ạnKiểu APIKiểu dữ liệu
CookiesYes — every matching request~4 KB per cookieSet-Cookie; document.cookie sync cho cookie JS-visibleChuỗi
sessionStorageNoBrowser quota; page session riêngSyncChuỗi
localStorageNoThường ~5 MiB/origin; phải bắt QuotaExceededErrorSyncChuỗi
IndexedDBNoQuota implementation-definedAsync (transactions)Structured-clone values
Cache APINoThường chung storage quota với IDBAsyncRequest / 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.


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ínhMục đích
HttpOnlyJS không đọc — giảm đánh cắp token XSS
SecureChỉ gửi qua HTTPS
SameSite=Lax/Strict/NoneKiểm soát gửi cross-site (giảm CSRF)
Max-Age / ExpiresThời hạn
Domain / PathPhạm vi matching request; Path không phải security boundary
PartitionedDouble-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 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ộ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 đó.

localStorage khô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:

  • Databaseobject 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; await cô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 version trong open() và migrate trong onupgradeneeded.

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ượcHành viPhù hợp
Cache-firstCache → network on missAsset tĩnh, app shell
Network-firstNetwork → cache fallbackDữ liệu API cần tươi
Stale-while-revalidateReturn cache immediately, update in backgroundNội dung bán tĩnh
Network-onlyBỏ qua Cache API; fetch() vẫn theo HTTP-cache semanticsRequest 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 Partitioningtracker.com trong iframe trên shop.com có bucket riêng với tracker.com trên news.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ầuDùngTránh
Session token for server authHttpOnly cookielocalStorage (XSS đọc được)
Theme / language preferencelocalStorageCookie (gửi mọi request)
Form draft for one tab sessionsessionStorageCookies
Offline docs, 10k+ records, binary blobsIndexedDBlocalStorage (size + sync)
Offline shell + cached assetsCache API + Service WorkerManual IDB of Response bodies
”Maybe next page” assetsHTTP 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.