Web Storage APIs - Lưu Dữ Liệu Client-Side Hiệu Quả Cho Ứng Dụng Web

Bạn đang build một ứng dụng web và muốn lưu dữ liệu ngay trên trình duyệt thay vì phải gọi server mỗi lần? Hoặc cần app hoạt động offline, giữ form data khi user reload trang, hay cache file tĩnh để tăng tốc? Web Storage APIs chính là giải pháp bạn cần.

Có nhiều cách lưu trữ khác nhau - từ localStorage đơn giản đến IndexedDB mạnh mẽ, mỗi loại phù hợp với từng tình huống cụ thể. Bài viết này sẽ giúp bạn hiểu rõ từng API, khi nào nên dùng cái nào, và cách áp dụng vào dự án thực tế với code examples chi tiết.

View Mermaid diagram code
flowchart TD
    Start([Cần lưu dữ liệu?]) --> Q1{Dung lượng data?}
    Q1 -->|Nhỏ < 5MB| Q2{Cần gửi lên server?}
    Q1 -->|Lớn > 5MB| IndexedDB[IndexedDB]

    Q2 -->|Có| Cookies[Cookies]
    Q2 -->|Không| Q3{Lưu lâu dài?}

    Q3 -->|Có - vĩnh viễn| LocalStorage[localStorage]
    Q3 -->|Không - chỉ trong tab| SessionStorage[sessionStorage]

    Q1 -->|File tĩnh CSS/JS/Images| CacheStorage[Cache Storage + Service Worker]

    IndexedDB --> Note1[Offline apps, PWA]
    Cookies --> Note2[Authentication, tracking]
    LocalStorage --> Note3[Settings, preferences]
    SessionStorage --> Note4[Form drafts, temp data]
    CacheStorage --> Note5[PWA, offline-first]

#1. localStorage

#1.1. Cách hoạt động

localStorage là một dạng lưu trữ dữ liệu dạng key-value (cặp khóa-giá trị) trên trình duyệt, cho phép giữ dữ liệu lâu dài, ngay cả khi bạn đóng trình duyệt. Dữ liệu chỉ mất khi người dùng xóa thủ công hoặc khi code xóa nó. Dung lượng của localStorage thường lên đến khoảng 5MB (tùy trình duyệt).

  • Mỗi trang web chỉ có quyền truy cập vào localStorage của chính nó (theo domain).
  • Chỉ chấp nhận kiểu dữ liệu chuỗi (string). Muốn lưu đối tượng, bạn cần chuỗi hóa (JSON.stringify) và khi lấy ra cần chuyển ngược về đối tượng (JSON.parse).

#1.2. Ví dụ cơ bản

1
2
3
4
5
6
7
8
9
10
11
12
13
// Lưu dữ liệu đơn giản
localStorage.setItem("username", "NguyenVanA");
localStorage.setItem("theme", "dark");

// Lấy dữ liệu
const username = localStorage.getItem("username");
console.log(username); // "NguyenVanA"

// Xóa một item
localStorage.removeItem("theme");

// Xóa toàn bộ localStorage
localStorage.clear();

Giải thích:

  • setItem(key, value) - Lưu dữ liệu
  • getItem(key) - Lấy dữ liệu
  • removeItem(key) - Xóa một item
  • clear() - Xóa toàn bộ

#1.3. Ví dụ thực tế - Lưu cài đặt giao diện

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Lưu object (phải chuyển sang JSON)
const userSettings = {
  theme: "dark",
  language: "vi",
  fontSize: 16
};

localStorage.setItem("settings", JSON.stringify(userSettings));

// Lấy object ra
const savedSettings = localStorage.getItem("settings");
if (savedSettings) {
  const settings = JSON.parse(savedSettings);
  console.log(settings.theme); // "dark"
}

// Cập nhật một phần settings
const currentSettings = JSON.parse(localStorage.getItem("settings") || "{}");
currentSettings.theme = "light";
localStorage.setItem("settings", JSON.stringify(currentSettings));

Trong thực tế:

  • Dùng để lưu theme (dark/light mode)
  • Lưu ngôn ngữ user đã chọn
  • Lưu trạng thái sidebar (mở/đóng)
  • Lưu form data tạm thời (draft)

#Lưu ý quan trọng:

  • Chỉ lưu string: Phải dùng JSON.stringify() cho object/array
  • Dung lượng giới hạn: ~5MB (tùy browser)
  • Synchronous: Có thể block UI nếu data lớn
  • Không bảo mật: Đừng lưu mật khẩu, token nhạy cảm

#2. sessionStorage

#2.1. Cách hoạt động

sessionStorage cũng tương tự localStorage, nhưng dữ liệu sẽ chỉ tồn tại trong phiên làm việc (tab) hiện tại. Một khi bạn đóng tab hoặc tải lại trang trong một tab mới, dữ liệu sẽ mất.

  • Cùng site, nhưng mỗi tab có sessionStorage riêng.
  • Dung lượng cũng ở mức vài MB (tùy trình duyệt).
  • Chỉ lưu trữ dưới dạng chuỗi.
  • Lưu ý: Nếu bạn duplicate (nhân đôi) tab, một số trình duyệt sẽ copy dữ liệu sessionStorage của tab cũ sang tab mới, khiến bạn có cảm giác “nhân đôi” session. Hành vi này có thể khác nhau tùy vào từng trình duyệt.

#2.2. Ví dụ thực tế - Lưu dữ liệu form tạm thời

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Lưu draft khi user đang điền form
const saveFormDraft = () => {
  const formData = {
    title: document.getElementById("title").value,
    content: document.getElementById("content").value,
    timestamp: Date.now()
  };

  sessionStorage.setItem("formDraft", JSON.stringify(formData));
};

// Tự động lưu mỗi 2 giây
setInterval(saveFormDraft, 2000);

// Khôi phục draft khi load trang
window.addEventListener("DOMContentLoaded", () => {
  const draft = sessionStorage.getItem("formDraft");
  if (draft) {
    const data = JSON.parse(draft);
    document.getElementById("title").value = data.title;
    document.getElementById("content").value = data.content;
  }
});

// Xóa draft khi submit thành công
const handleSubmit = () => {
  // Submit form...
  sessionStorage.removeItem("formDraft");
};

Trong thực tế:

  • Lưu form draft trong tab (mất khi đóng tab)
  • Lưu trạng thái wizard/multi-step form
  • Lưu filter/search params tạm thời
  • Lưu data cho tab riêng lẻ (không ảnh hưởng tab khác)

#Khác biệt với localStorage:

  • sessionStorage: Mất khi đóng tab (phù hợp cho data tạm)
  • localStorage: Lưu vĩnh viễn (phù hợp cho settings)

#3. Cookies

#3.1. Cách hoạt động

Cookies là cách lưu trữ dữ liệu nhỏ trên trình duyệt từ những ngày đầu của web. Thông thường, cookies được gửi kèm với mỗi request đến máy chủ, giúp theo dõi phiên đăng nhập, phân tích hành vi người dùng, v.v.

  • Mỗi cookie có thể thiết lập thời gian hết hạn (expires), đường dẫn (path) và domain.
  • Dung lượng tối đa ~4KB trên mỗi cookie.
  • Thường sử dụng cho mục đích xác thực (login session), theo dõi người dùng (tracking), cài đặt người dùng (preferences).
View Mermaid diagram code
sequenceDiagram
    participant Browser
    participant Server

    Browser->>Server: Request đầu tiên (login)
    Server-->>Browser: Set-Cookie: sessionId=abc123
    Note over Browser: Lưu cookie vào browser

    Browser->>Server: Request tiếp theo (tự động gửi cookie)
    Note over Browser,Server: Cookie: sessionId=abc123
    Server-->>Browser: Response (đã xác thực)

    Browser->>Server: Request khác (cookie vẫn gửi kèm)
    Note over Browser,Server: Cookie: sessionId=abc123
    Server-->>Browser: Response

#3.2. Ví dụ thực tế - Helper functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Helper: Set cookie
const setCookie = (name, value, days = 7) => {
  const expires = new Date(Date.now() + days * 864e5).toUTCString();
  document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Strict`;
};

// Helper: Get cookie
const getCookie = (name) => {
  return document.cookie.split("; ").reduce((r, v) => {
    const parts = v.split("=");
    return parts[0] === name ? decodeURIComponent(parts[1]) : r;
  }, "");
};

// Helper: Delete cookie
const deleteCookie = (name) => {
  setCookie(name, "", -1);
};

// Sử dụng
setCookie("userTheme", "dark", 30); // Lưu 30 ngày
const theme = getCookie("userTheme");
console.log(theme); // "dark"

deleteCookie("userTheme"); // Xóa cookie

Trong thực tế:

  • Lưu session token (authentication)
  • Lưu user preferences (ngôn ngữ, currency)
  • Tracking analytics (Google Analytics)
  • Remember me checkbox

#Lưu ý bảo mật:

  • Secure flag: Chỉ gửi qua HTTPS
  • HttpOnly: Không thể truy cập từ JavaScript (tránh XSS)
  • SameSite: Ngăn CSRF attacks
  • Dung lượng: Max 4KB/cookie
  • Đừng lưu: Mật khẩu, thông tin nhạy cảm
1
2
// Cookie bảo mật (set từ server-side)
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600

#4. IndexedDB

#4.1. Cách hoạt động

IndexedDB là một cơ sở dữ liệu (NoSQL) hoạt động trong trình duyệt. Nó cho phép lưu trữ lượng dữ liệu lớn hơn so với localStorage, có khả năng index, truy vấn và quản lý cấu trúc dữ liệu phức tạp.

  • Có thể lưu trữ nhiều dạng dữ liệu, bao gồm cả BLOB (Binary Large Object).
  • Thích hợp cho ứng dụng web cần hoạt động ngoại tuyến hoặc thao tác dữ liệu lớn.
  • Giao thức bất đồng bộ (asynchronous), tương tác qua Promise hoặc callback.

#4.2. Ví dụ (TypeScript)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
interface User {
  id?: number;
  name: string;
  age: number;
}

class MyIndexedDB {
  private static db: IDBDatabase | null = null;

  public static init(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      // Nếu đã có db, trả về ngay
      if (MyIndexedDB.db) {
        return resolve(MyIndexedDB.db);
      }

      // Mở (hoặc tạo) DB "MyDatabase" phiên bản 1
      const request: IDBOpenDBRequest = indexedDB.open("MyDatabase", 1);

      request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
        const dbInstance = (event.target as IDBOpenDBRequest).result;

        // Tạo object store (bảng "users") nếu chưa có
        if (!dbInstance.objectStoreNames.contains("users")) {
          dbInstance.createObjectStore("users", {
            keyPath: "id",
            autoIncrement: true
          });
        }
      };

      request.onsuccess = (event: Event) => {
        MyIndexedDB.db = (event.target as IDBOpenDBRequest).result;
        console.log("Đã mở IndexedDB thành công (Singleton).");
        resolve(MyIndexedDB.db);
      };

      request.onerror = () => {
        reject("Không thể mở IndexedDB.");
      };
    });
  }

  private static async getDB(): Promise<IDBDatabase> {
    if (MyIndexedDB.db) {
      return MyIndexedDB.db;
    }
    return this.init();
  }

  public static async addUser(user: Omit<User, "id">): Promise<number> {
    const db = await MyIndexedDB.getDB();

    return new Promise((resolve, reject) => {
      const transaction: IDBTransaction = db.transaction("users", "readwrite");
      const store: IDBObjectStore = transaction.objectStore("users");
      const request: IDBRequest<IDBValidKey> = store.add(user);

      request.onsuccess = () => {
        // request.result chính là khóa chính (id) được auto-increment
        resolve(request.result as number);
      };
      request.onerror = () => {
        reject(request.error);
      };
    });
  }

  public static async getAllUsers(): Promise<User[]> {
    const db = await MyIndexedDB.getDB();

    return new Promise((resolve, reject) => {
      const transaction: IDBTransaction = db.transaction("users", "readonly");
      const store: IDBObjectStore = transaction.objectStore("users");
      const request: IDBRequest<User[]> = store.getAll() as IDBRequest<User[]>;

      request.onsuccess = () => {
        resolve(request.result);
      };
      request.onerror = () => {
        reject(request.error);
      };
    });
  }

  public static async getUserById(id: number): Promise<User | null> {
    const db = await MyIndexedDB.getDB();

    return new Promise((resolve, reject) => {
      const transaction: IDBTransaction = db.transaction("users", "readonly");
      const store: IDBObjectStore = transaction.objectStore("users");
      const request: IDBRequest<User> = store.get(id) as IDBRequest<User>;

      request.onsuccess = () => {
        // Nếu không tìm thấy, request.result = undefined => trả về null
        resolve(request.result || null);
      };
      request.onerror = () => {
        reject(request.error);
      };
    });
  }
}

async function main() {
  try {
    // 1. Khởi tạo (mở) DB (chỉ cần gọi 1 lần khi app khởi chạy)
    await MyIndexedDB.init();

    // 2. Thêm user
    const newUserId = await MyIndexedDB.addUser({ name: "Nguyen Van B", age: 25 });
    console.log("User mới có ID:", newUserId);

    // 3. Lấy toàn bộ user
    const allUsers = await MyIndexedDB.getAllUsers();
    console.log("Danh sách users:", allUsers);

    // 4. Lấy user theo ID
    const user = await MyIndexedDB.getUserById(newUserId);
    console.log("User vừa thêm:", user);

    // ... Tiếp tục các thao tác khác
  } catch (error) {
    console.error("Đã xảy ra lỗi:", error);
  }
}

main();

Kết quả:

1
2
3
4
Đã mở IndexedDB thành công (Singleton).
User mới có ID: 1
Danh sách users: [{name: 'Nguyen Van B', age: 25, id: 1}]
User vừa thêm: {name: 'Nguyen Van B', age: 25, id: 1}

Trong thực tế:

  • Lưu email drafts offline (Gmail)
  • Cache data cho PWA
  • Lưu media files (ảnh, video, audio)
  • Offline-first apps (todo apps, note apps)

Khi nào dùng IndexedDB?

  • Data > 5MB (vượt localStorage limit)
  • Cần query phức tạp (filter, sort, index)
  • Offline functionality
  • Lưu binary data (images, files)

Ưu điểm: Dung lượng lớn (hàng trăm MB), query mạnh mẽ, async (không block UI).
Nhược điểm: API phức tạp, cần wrapper library (Dexie.js, localForage).

#5. Cache Storage

#5.1. Service Worker là gì?

Service Worker là một script chạy dưới nền trong trình duyệt, tách biệt với trang web chính. Nó có thể can thiệp vào luồng request/response, cho phép lưu trữ file tĩnh (HTML, CSS, JS, ảnh…) vào bộ nhớ đệm (cache) để hỗ trợ chức năng ngoại tuyến (offline).

#5.2. Cách hoạt động

Cache Storage thường được quản lý thông qua Service Worker. Khi browser gửi request, Service Worker sẽ kiểm tra cache trước, nếu có thì trả về từ cache, nếu không mới gọi network. Bạn có thể tùy chỉnh cơ chế này (cache-first, network-first, stale-while-revalidate, v.v.).

View Mermaid diagram code
flowchart LR
    Request([Browser Request]) --> SW[Service Worker]
    SW --> CheckCache{Có trong cache?}

    CheckCache -->|Có| ReturnCache[Trả về từ cache]
    CheckCache -->|Không| FetchNetwork[Fetch từ network]

    FetchNetwork --> SaveCache[Lưu vào cache]
    SaveCache --> ReturnNetwork[Trả về response]

    ReturnCache --> Done([Response đến browser])
    ReturnNetwork --> Done

    style ReturnCache fill:#90EE90
    style FetchNetwork fill:#FFB6C1

#5.3. Ví dụ

File index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Cache Storage</title>
  <link rel="stylesheet" href="styles.css">
  <script src="main.js"></script>
</head>
<body>
  <h1>Cache Storage</h1>
  <button id="clearCache">Xóa cache trong service workers</button>
  <script>
    // Đăng ký Service Worker
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("service-worker.js")
        .then(() => console.log("Service Worker đã đăng ký!"))
        .catch(err => console.error("Lỗi SW:", err));
    }

    const clearCacheButton = document.getElementById('clearCache');
    clearCacheButton.addEventListener('click', function(){
      navigator.serviceWorker.controller?.postMessage({ type: "CLEAR_OLD_CACHE" });
      setTimeout(() => {
        location.reload();
      }, 1000)
    });
  </script>
</body>
</html>

File styles.css:

1
2
3
h1 {
  color: red;
}

File main.js:

1
console.log('Hello');

File service-worker.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const CACHE_NAME = "my-cache-v1";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(["/", "/index.html", "/styles.css", "/main.js"]);
    })
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

self.addEventListener("message", (event) => {
  if (event.data && event.data.type === "CLEAR_OLD_CACHE") {
    caches.delete(CACHE_NAME).then((wasDeleted) => {
      console.log(`Xóa ${CACHE_NAME}:`, wasDeleted);
    });
  }
});

Các file index.html, styles.cssmain.js sẽ được lưu vào Cache Storage. Khi offline, trình duyệt vẫn có thể tải những file này từ cache, nâng cao trải nghiệm người dùng.

Trong thực tế:

  • Progressive Web Apps (PWA)
  • Cache static assets (CSS, JS, fonts, images)
  • Offline-first strategy
  • Tăng tốc load time (load từ cache thay vì network)

Lưu ý:

  • Cần HTTPS để dùng Service Worker (trừ localhost)
  • Quản lý cache versions để tránh serving stale files
  • Combine với IndexedDB cho data, Cache Storage cho assets

#6. File System Access API

#6.1. Cách hoạt động

File System Access API (trước đây là Native File System API) cho phép trang web tương tác với tệp trên máy tính người dùng gần giống như ứng dụng desktop. Bạn có thể mở, ghi, sửa tệp trực tiếp, tạo trải nghiệm mạnh mẽ trên web.

  • Được hỗ trợ tốt trên Chrome, Edge, nhưng chưa phổ biến trên các trình duyệt khác.
  • Cần thao tác đồng ý từ người dùng (permission) để truy cập thư mục hoặc tệp trên máy tính.

#6.2. Ví dụ thực tế

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Đọc file từ máy tính
async function openFile() {
  try {
    const [fileHandle] = await window.showOpenFilePicker({
      types: [{
        description: "Text Files",
        accept: { "text/plain": [".txt"] }
      }]
    });

    const file = await fileHandle.getFile();
    const content = await file.text();
    console.log("Nội dung:", content);
    return content;
  } catch (err) {
    console.error("User cancelled:", err);
  }
}

// Lưu file xuống máy tính
async function saveFile(content) {
  try {
    const fileHandle = await window.showSaveFilePicker({
      suggestedName: "document.txt",
      types: [{
        description: "Text Files",
        accept: { "text/plain": [".txt"] }
      }]
    });

    const writable = await fileHandle.createWritable();
    await writable.write(content);
    await writable.close();

    console.log("Đã lưu file!");
  } catch (err) {
    console.error("User cancelled:", err);
  }
}

// Sử dụng
document.getElementById("openBtn").addEventListener("click", openFile);
document.getElementById("saveBtn").addEventListener("click", () => {
  saveFile("Nội dung mới...");
});

Trong thực tế:

  • Text/code editors (VS Code for Web)
  • Image editors (Photopea, Figma)
  • Video/audio editors
  • File management apps

Lưu ý:

  • Chỉ Chrome/Edge hỗ trợ đầy đủ (2026)
  • Cần user permission mỗi lần truy cập
  • Không thay thế được desktop apps hoàn toàn
  • Bảo mật: Luôn xin phép user trước khi truy cập file

#7. Bảng so sánh

Dưới đây là so sánh về thời gian tồn tại của dữ liệu:

View Mermaid diagram code
flowchart TD
    subgraph Persistent["Lưu trữ vĩnh viễn (đến khi xóa thủ công)"]
        LS[localStorage]
        IDB[IndexedDB]
        CS[Cache Storage]
    end

    subgraph Temporary["Tạm thời (hết hạn tự động)"]
        SS[sessionStorage: Mất khi đóng tab]
        Cookie[Cookies: Hết hạn theo expires/max-age]
    end

    subgraph FileSystem["Truy cập file hệ thống"]
        FS[File System Access API: Truy cập trực tiếp]
    end

    style LS fill:#90EE90
    style IDB fill:#90EE90
    style CS fill:#90EE90
    style SS fill:#FFD700
    style Cookie fill:#FFD700
    style FS fill:#87CEEB

Bảng so sánh chi tiết các công nghệ Web Storage:

Công nghệUse CasesƯu điểmNhược điểmKhi nào nên dùng?
localStorageLưu dữ liệu nhỏ, cấu hình người dùngĐơn giản, dễ sử dụng, dung lượng tầm 5MBChỉ lưu dạng chuỗi, đồng bộCài đặt giao diện, token tạm thời, thông tin form v.v.
sessionStorageLưu dữ liệu trong 1 phiên (tab)Dữ liệu chỉ tồn tại trong tab, hạn chế xung đột giữa các tabChỉ lưu dạng chuỗi, dung lượng tầm vài MB, mất dữ liệu khi đóng tab
Lưu ý duplicate tab có thể sao chép session.
Khi cần lưu dữ liệu tạm theo từng tab, ví dụ giỏ hàng, thông tin xem nhanh
CookiesDữ liệu nhỏ, thường dùng để xác thựcTương thích rộng, tự động gửi lên serverGiới hạn 4KB, bảo mật cần HTTPS, gửi lên server mọi requestLưu session đăng nhập, tùy chọn người dùng (remember me)
IndexedDBỨng dụng offline, lưu nhiều dữ liệu phức tạpKhả năng lưu trữ lớn, hỗ trợ indexingAPI phức tạp, cần xử lý bất đồng bộỨng dụng lớn, cần lưu dữ liệu cục bộ, chế độ offline
Cache StorageLưu file tĩnh (HTML, CSS, JS, ảnh)Phù hợp với Service Worker, tối ưu ngoại tuyếnCần cài đặt Service Worker, quản lý phiên bản cache phức tạpTạo PWA, tăng tốc độ tải, hỗ trợ offline
File System Access APITương tác tệp hệ thốngTrải nghiệm như ứng dụng desktop, đọc/ghi file trực tiếpChưa hỗ trợ trên tất cả trình duyệt, yêu cầu quyền người dùngỨng dụng chỉnh sửa ảnh, nhạc, văn bản trực tiếp trên máy

Vậy là bạn đã hiểu cách hoạt động của từng Web Storage API. Trong thực tế, hãy chọn API phù hợp với nhu cầu: localStorage/sessionStorage cho settings và form data nhỏ, IndexedDB khi cần lưu data lớn hoặc query phức tạp, Cache Storage cho PWA và offline apps, Cookies cho authentication.

Đừng quên: localStorage cho đơn giản, IndexedDB cho mạnh mẽ, Cache Storage cho tốc độ. Kết hợp đúng cách sẽ giúp app của bạn nhanh hơn, hoạt động offline, và giảm tải server đáng kể!