Web browser standard storage API web-api
Browser storage không phải là một cái kho duy nhất. Mỗi loại storage có mô hình dữ liệu, vòng đời, mức độ gắn với HTTP và chi phí khác nhau. Câu hỏi đầu tiên không phải là “API nào dễ dùng nhất?”, mà là:
- Dữ liệu có cần server đọc trực tiếp không?
- Dữ liệu là vài giá trị nhỏ hay một tập dữ liệu lớn có cấu trúc?
- Dữ liệu là response HTTP, file riêng của app, hay file người dùng nhìn thấy trên máy?
- Nếu trình duyệt xóa dữ liệu để giải phóng dung lượng thì app có khôi phục được không?
Bức tranh tổng quan
IndexedDB, Cache Storage, OPFS và Web Storage thường nằm trong hệ thống storage theo origin của browser. Browser quản lý quota chung cho nhiều loại dữ liệu và có thể eviction dữ liệu khi thiếu dung lượng. Vì vậy “đã ghi thành công” không đồng nghĩa với “được backup vĩnh viễn”.
Origin, partition và vòng đời
Origin là ranh giới chính
Origin gồm scheme + host + port. Ví dụ, ba URL sau là ba origin khác nhau:
localStorage, sessionStorage, IndexedDB, Cache Storage và OPFS về cơ bản bị giới hạn bởi same-origin policy. JavaScript ở https://app.example.com không tự đọc được IndexedDB của https://api.example.com.
Cookie có mô hình scope khác: Domain, Path và các thuộc tính SameSite quyết định cookie được gửi trong request nào. Đây là lý do cookie là HTTP state mechanism chứ không chỉ là một key-value store cho JavaScript.
Session, persistence và eviction
- Session: dữ liệu mất khi session tương ứng kết thúc, ví dụ
sessionStoragekhi tab đóng. - Persistent: dữ liệu được browser giữ qua lần mở lại, nhưng user vẫn có thể xóa site data.
- Eviction: browser chủ động xóa dữ liệu không persistent khi cần giải phóng dung lượng.
- Clear site data: thao tác của user có thể xóa toàn bộ dữ liệu của origin, bất kể app dùng API nào.
Không nên xem browser storage là nơi duy nhất chứa dữ liệu không thể mất. Dữ liệu quan trọng vẫn cần server sync hoặc cơ chế export/backup.
Nhóm 1: Cookies
Cookie là cơ chế lưu trạng thái giữa browser và server. Cookie có thể được browser tự động gửi trong HTTP request qua header Cookie, và server tạo/cập nhật cookie qua header Set-Cookie.
Khi nào dùng cookie?
Dùng cookie khi server cần biết trạng thái trong request, ví dụ:
- session identifier;
- refresh token hoặc một credential có chiến lược bảo mật phù hợp;
- preference nhỏ mà server cần đọc;
- một số trường hợp tracking/attribution tuân thủ privacy policy.
Không dùng cookie làm database phía client. Cookie được gửi cùng request phù hợp, nên dữ liệu thừa sẽ làm request lớn hơn.
Đọc và ghi bằng document.cookie
document.cookie là synchronous và API chuỗi này khá dễ viết sai khi parse. Một helper tối thiểu:
Các thuộc tính cần nhớ
Cookie có HttpOnly vẫn có thể bị gửi kèm request, chỉ là script không đọc được. HttpOnly không tự giải quyết CSRF; vẫn phải thiết kế SameSite, CSRF token và kiểm tra origin/referer khi phù hợp.
Cookie và fetch
Request same-origin mặc định gửi cookie. Request cross-origin cần cấu hình credentials và server phải cho phép theo CORS:
credentials: "include" không bỏ qua cookie policy, SameSite hay CORS. Nó chỉ nói với Fetch rằng request được phép tham gia credential flow nếu các policy khác cho phép.
Nhóm 2: Web Storage API
Web Storage có hai storage area:
localStorage: chia sẻ giữa các document cùng origin và tồn tại qua lần mở lại browser.sessionStorage: gắn với origin và browser tab; đóng tab thì dữ liệu thường bị hủy.
Cả hai đều synchronous và chỉ lưu string. Đây là điểm quan trọng: một object không được lưu trực tiếp như object; phải serialize bằng JSON hoặc một encoding khác.
Các method chung của Storage
Lưu object an toàn hơn
Khi thay đổi schema, dùng version trong key hoặc migrate rõ ràng. Đừng giả định JSON.parse luôn thành công: user, extension hoặc phiên bản app cũ có thể để lại dữ liệu không đúng format.
Đồng bộ state giữa các tab
Event storage chạy ở document khác đang dùng cùng storage area, không chạy lại trên window vừa gọi setItem:
Nếu cần giao tiếp hai chiều có cấu trúc và không muốn lạm dụng storage event, cân nhắc BroadcastChannel. Nó là communication API, không phải storage API.
Khi nào không dùng Web Storage?
Không dùng localStorage/sessionStorage cho:
- danh sách lớn hoặc dữ liệu cần query/index;
- dữ liệu nhị phân lớn;
- thao tác thường xuyên trên main thread;
- access token, mật khẩu hoặc thông tin nhạy cảm;
- dữ liệu cần transaction nhiều bước.
Vì thao tác sync có thể block main thread và quota nhỏ, Web Storage phù hợp với preference/config nhỏ hơn là application database.
Nhóm 3: IndexedDB
IndexedDB là database async phía client. Nó lưu được structured data theo structured clone algorithm, có object store, key, index và transaction. Đây thường là lựa chọn mặc định cho dữ liệu app offline có cấu trúc.
Mô hình cần nhớ
Schema được nâng version bằng indexedDB.open(name, version). Khi version tăng, onupgradeneeded chạy để tạo hoặc migrate object store/index.
Mở database và migrate schema
Native IndexedDB dùng event callback, nên có thể bọc IDBRequest thành Promise:
Khi tăng version, connection cũ nên đóng ở mọi tab. Có thể gắn handler ngay sau khi mở database:
Ghi, đọc và xóa record
IDBTransaction có lifecycle riêng, nên helper cho transaction cần lắng nghe complete, error và abort:
Query bằng index
Khi cần dữ liệu theo range, dùng IDBKeyRange, ví dụ IDBKeyRange.lowerBound(timestamp). Transaction nên ngắn; không chờ network trong một transaction rồi mới ghi tiếp, vì transaction có thể trở nên inactive.
IndexedDB checklist
- Schema migration chỉ đặt trong
onupgradeneeded. - Đặt
keyPathvà index ngay từ đầu nếu biết cách query. - Đóng connection khi nhận
versionchange. - Bắt lỗi
QuotaExceededError,AbortError,VersionErrorvàNotFoundErrorkhi phù hợp. - Không lưu secret chỉ vì IndexedDB “khó mở hơn localStorage”; XSS vẫn có thể đọc dữ liệu bằng JavaScript của origin.
- Nếu code nhiều, dùng một wrapper đã được kiểm chứng để giảm callback/event boilerplate, nhưng vẫn phải hiểu transaction model của IndexedDB.
Nhóm 4: Cache API / Cache Storage
Cache Storage lưu cặp Request/Response. Nó được thiết kế cho cache HTTP, đặc biệt trong Service Worker; nó không phải database tùy ý và cũng không tự động cache mọi request.
API cơ bản
Có thể lưu response tạo thủ công, nhưng phải nhớ response body thường chỉ đọc được một lần:
Cache trong Service Worker
Đây là cache-first tối giản. Production cần quyết định rõ strategy cho từng loại request:
- app shell: precache/cache-first;
- API có thể thay đổi: network-first hoặc stale-while-revalidate;
- response không muốn giữ: không cache;
- POST/PUT/DELETE: không dùng cache như thể là database.
Version hóa cache và xóa cache cũ trong activate; nếu không, bản build cũ có thể tồn tại mãi cho tới khi user tự xóa site data.
Nhóm 5: Origin Private File System (OPFS)
OPFS là file system private của origin. File không xuất hiện trong Finder/Explorer của user và không cần user chọn file. Đây là sandbox của app, phù hợp với file lớn hoặc thao tác byte-level hiệu năng cao.
OPFS khác với File System Access API:
Ghi và đọc file trong OPFS
Để tạo thư mục:
OPFS chịu quota của origin. Một số API sync như FileSystemSyncAccessHandle chỉ dùng được trong dedicated Web Worker, để không block main thread. Đây là hướng phù hợp cho SQLite/WASM, editor hoặc xử lý media nhiều byte; không nên đưa sync handle lên main thread.
Nhóm 6: File System Access API
File System Access API cho web app tương tác với file/directory thật mà user chọn. Đây là API có permission và browser support không đồng đều, nên luôn feature-detect và có fallback <input type="file"> hoặc download.
Mở file do user chọn
Các picker thường phải được gọi sau một user gesture, ví dụ bên trong click handler:
Lưu file
Nếu lưu FileSystemFileHandle vào IndexedDB, lần sau app có thể lấy handle lại rồi kiểm tra permission. Tuy nhiên quyền có thể bị browser/user thu hồi:
Không nên tự động gọi picker khi page load. Hãy để user chủ động chọn “Open” hoặc “Save”, giải thích app sẽ làm gì với file, và luôn xử lý trường hợp user cancel.
Nhóm 7: Storage API và StorageManager
navigator.storage không phải nơi lưu dữ liệu mới. Nó là entry point để quan sát và điều khiển một phần lifecycle của storage theo origin.
Ước lượng usage và quota
usage và quota là ước lượng, không nên dùng như số liệu chính xác tuyệt đối. Các browser có cách tính quota khác nhau và một số usage có thể được làm tròn vì privacy.
Persistent storage
persist() là request, không phải mệnh lệnh. Browser có thể tự quyết định false dựa trên engagement, permission, dung lượng và policy. Persistent cũng không ngăn user chủ động xóa site data.
Dùng feature detection
Storage API và OPFS thường yêu cầu secure context (https hoặc môi trường local được browser xem là an toàn). Worker cũng có thể có WorkerNavigator.storage trong các browser hỗ trợ.
Nhóm 8: Storage Buckets
Storage Buckets là hướng thiết kế để một origin chia dữ liệu thành các bucket có policy riêng: quota, expiration, persistence và eviction. Ví dụ app mail có thể tách drafts, attachments và cache để cache bị xóa trước dữ liệu quan trọng.
Tại thời điểm viết bài, đây vẫn là draft/experimental API, không phải nền tảng browser phổ biến để dùng mặc định. Nếu thử nghiệm, phải feature-detect:
Không xây thiết kế production chỉ dựa vào API này nếu chưa kiểm tra browser target, compatibility và fallback.
Chọn API nào?
Một app thực tế thường phối hợp nhiều API:
Đừng lưu cùng một dữ liệu ở nhiều nơi nếu không có lý do. Nếu phải làm vậy, xác định rõ source of truth và quy tắc invalidation; nếu không, app sẽ có các bản copy lệch nhau.
Mẫu wrapper nhỏ cho storage key-value
Khi chỉ cần preference, một wrapper giúp thống nhất JSON, default value và xử lý lỗi:
Wrapper này chỉ dành cho dữ liệu nhỏ. Không biến nó thành lớp abstraction giả cho IndexedDB; hai API có semantics hoàn toàn khác nhau.
Security và privacy
Đừng lưu secret ở client storage nếu không cần
JavaScript chạy cùng origin có thể đọc localStorage, sessionStorage, IndexedDB, Cache Storage và OPFS. Nếu app có XSS, attacker có thể đọc hoặc sửa dữ liệu trong các kho này.
- Không lưu password trong browser storage.
- Cân nhắc cookie
HttpOnly; Secure; SameSite=Lax/Strictcho session server-side. - Access token trong memory có thể giảm thời gian tồn tại nhưng vẫn có trade-off khi reload.
- CSP, output encoding, dependency hygiene và XSS prevention quan trọng hơn việc chọn một storage “khó đọc”.
Đừng tin dữ liệu đọc lại từ storage
Dữ liệu trong storage có thể bị user, extension, phiên bản app cũ hoặc chính code khác sửa. Luôn validate shape và range trước khi dùng:
Third-party và private browsing
Iframe third-party có thể bị partition hoặc bị chặn storage tùy browser và policy privacy. Private/incognito mode thường có quota/lifecycle khác và có thể làm một số thao tác storage throw error. Hãy test đúng browser target thay vì giả định mọi browser giống nhau.
Checklist khi thiết kế storage
- Dữ liệu có cần server đọc không? Nếu có, xem xét cookie hoặc gửi explicit trong request.
- Dữ liệu có nhỏ và ít truy cập không? Nếu có, Web Storage có thể đủ.
- Dữ liệu có nhiều record, index, transaction hoặc Blob không? Chọn IndexedDB.
- Dữ liệu là HTTP response cần offline không? Chọn Cache Storage.
- Dữ liệu là file nội bộ lớn hay file user chọn? Phân biệt OPFS và File System Access.
- Có fallback khi API không tồn tại, permission bị từ chối hoặc quota hết không?
- Có schema/version/migration không?
- Có validate dữ liệu đọc lại không?
- App có hoạt động đúng nếu toàn bộ site data bị xóa không?
- Dữ liệu có cần sync server/export để tránh mất không?