Bộ nhớ dành cho web

Có nhiều lựa chọn để lưu trữ dữ liệu trong trình duyệt. Lựa chọn nào phù hợp nhất với nhu cầu của bạn?

Kết nối Internet có thể không ổn định hoặc không có khi di chuyển. Đó là lý do tại sao tính năng hỗ trợ ngoại tuyến và hiệu suất đáng tin cậy là những tính năng phổ biến trong ứng dụng web tiến bộ. Ngay cả trong môi trường không dây hoàn hảo, việc sử dụng bộ nhớ đệm và các kỹ thuật lưu trữ khác một cách hợp lý cũng có thể cải thiện đáng kể trải nghiệm người dùng. Có một số cách để lưu các tài nguyên ứng dụng tĩnh (HTML, JavaScript, CSS, hình ảnh, v.v.) và dữ liệu (dữ liệu người dùng, bài viết tin tức, v.v.) vào bộ nhớ đệm. Nhưng giải pháp nào là tốt nhất? Bạn có thể lưu trữ bao nhiêu? Làm cách nào để ngăn việc bị loại bỏ?

Dưới đây là đề xuất chung về cách lưu trữ tài nguyên:

IndexedDB, OPFS và API Bộ nhớ đệm được hỗ trợ trong mọi trình duyệt hiện đại. Các luồng này không đồng bộ và sẽ không chặn luồng chính (nhưng cũng có một biến thể đồng bộ của OPFS chỉ có trong trình chạy web). Bạn có thể truy cập vào các đối tượng này từ đối tượng window, worker web và worker dịch vụ, nhờ đó có thể sử dụng các đối tượng này ở bất kỳ đâu trong mã.

Còn các cơ chế lưu trữ khác thì sao?

Có một số cơ chế lưu trữ khác có sẵn trong trình duyệt, nhưng các cơ chế này chỉ có thể sử dụng ở mức hạn chế và có thể gây ra các vấn đề đáng kể về hiệu suất.

SessionStorage dành riêng cho thẻ và thuộc phạm vi thời gian hoạt động của thẻ. Việc này có thể hữu ích khi lưu trữ một lượng nhỏ thông tin cụ thể về phiên, ví dụ: khoá IndexedDB. Bạn nên thận trọng khi sử dụng thuộc tính này vì trình xử lý này có tính đồng bộ và sẽ chặn luồng chính. Tệp này bị giới hạn ở mức khoảng 5 MB và chỉ có thể chứa chuỗi. Vì là thuộc tính dành riêng cho thẻ nên bạn không thể truy cập vào thuộc tính này từ worker web hoặc worker dịch vụ.

Bạn nên tránh sử dụng LocalStorage vì tính năng này có tính đồng bộ và sẽ chặn luồng chính. Tệp này bị giới hạn ở khoảng 5 MB và chỉ có thể chứa các chuỗi. Trình chạy dịch vụ hoặc trình thực thi dịch vụ không thể truy cập vào LocalStorage.

Cookie có mục đích sử dụng của chúng nhưng không nên dùng để lưu trữ. Cookie được gửi cùng với mọi yêu cầu HTTP, vì vậy, việc lưu trữ bất kỳ nội dung nào nhiều hơn một lượng nhỏ dữ liệu sẽ làm tăng đáng kể kích thước của mọi yêu cầu web. Các phương thức này đồng bộ và không thể truy cập được từ worker web. Giống như LocalStorage và SessionStorage, cookie chỉ được giới hạn ở các chuỗi.

API Truy cập hệ thống tệp được thiết kế để giúp người dùng có thể đọc và chỉnh sửa tệp trên hệ thống tệp cục bộ của họ. Người dùng phải cấp quyền trước khi một trang có thể đọc hoặc ghi vào bất kỳ tệp cục bộ nào và các quyền sẽ không được duy trì trong các phiên, trừ phi một tay cầm tệp được lưu vào bộ nhớ đệm trong IndexedDB. API Truy cập hệ thống tệp phù hợp nhất với các trường hợp sử dụng như trình chỉnh sửa, trong đó bạn cần mở một tệp, sửa đổi tệp đó rồi có thể lưu lại các thay đổi đối với tệp.

File System API (API Hệ thống tệp) và FileWriter API cung cấp các phương thức để đọc và ghi tệp vào một hệ thống tệp trong hộp cát. Mặc dù không đồng bộ, nhưng bạn không nên sử dụng phương thức này vì phương thức này chỉ có trong các trình duyệt dựa trên Chromium.

Tôi có thể lưu trữ bao nhiêu?

Tóm lại, rất nhiều, ít nhất là vài trăm megabyte và có thể lên đến hàng trăm gigabyte trở lên. Các cách triển khai trình duyệt có thể khác nhau, nhưng dung lượng lưu trữ có sẵn thường dựa trên dung lượng lưu trữ có sẵn trên thiết bị.

  • Chrome cho phép trình duyệt sử dụng tới 80% tổng dung lượng ổ đĩa. Một nguồn gốc có thể sử dụng tới 60% tổng dung lượng ổ đĩa. Bạn có thể sử dụng API StorageManager để xác định hạn mức tối đa hiện có. Các trình duyệt khác dựa trên Chromium có thể khác.
    • Ở chế độ ẩn danh, Chrome sẽ giảm mức dung lượng lưu trữ mà một nguồn gốc có thể sử dụng xuống khoảng 5% tổng dung lượng ổ đĩa.
    • Nếu người dùng đã bật tuỳ chọn "Xoá cookie và dữ liệu trang web khi bạn đóng tất cả cửa sổ" trong Chrome, thì hạn mức bộ nhớ sẽ giảm đáng kể xuống còn tối đa khoảng 300 MB.
  • Firefox cho phép trình duyệt sử dụng tối đa 50% dung lượng ổ đĩa trống. Một nhóm eTLD+1 (ví dụ: example.com, www.example.comfoo.bar.example.com) có thể sử dụng tối đa 2GB. Bạn có thể sử dụng API StorageManager để xác định dung lượng còn trống.
  • Safari (cả máy tính và thiết bị di động) có vẻ như cho phép khoảng 1 GB. Khi đạt đến giới hạn, Safari sẽ nhắc người dùng, tăng giới hạn theo mức tăng 200 MB. Tôi không tìm thấy tài liệu chính thức nào về vấn đề này.
    • Nếu bạn thêm một PWA vào màn hình chính trên Safari dành cho thiết bị di động, thì PWA đó sẽ tạo một vùng chứa bộ nhớ mới và không có nội dung nào được chia sẻ giữa PWA và Safari dành cho thiết bị di động. Sau khi đạt đến hạn mức cho một PWA đã cài đặt, dường như không có cách nào để yêu cầu thêm bộ nhớ.

Trước đây, nếu một trang web vượt quá ngưỡng nhất định về dữ liệu được lưu trữ, trình duyệt sẽ nhắc người dùng cấp quyền sử dụng thêm dữ liệu. Ví dụ: nếu nguồn gốc sử dụng nhiều hơn 50 MB, thì trình duyệt sẽ nhắc người dùng cho phép lưu trữ tối đa 100 MB, sau đó hỏi lại theo mức tăng 50 MB.

Ngày nay, hầu hết các trình duyệt hiện đại sẽ không nhắc người dùng và sẽ cho phép một trang web sử dụng hết hạn mức được phân bổ. Có vẻ như Safari, một trường hợp ngoại lệ sẽ nhắc khi vượt quá hạn mức bộ nhớ và yêu cầu quyền tăng hạn mức được phân bổ. Nếu một nguồn gốc cố gắng sử dụng nhiều hơn hạn mức được phân bổ, thì các lần thử ghi dữ liệu tiếp theo sẽ không thành công.

Làm cách nào để kiểm tra dung lượng lưu trữ còn trống?

Trong nhiều trình duyệt, bạn có thể sử dụng API StorageManager để xác định dung lượng lưu trữ có sẵn cho nguồn gốc và dung lượng lưu trữ mà nguồn gốc đó đang sử dụng. API này báo cáo tổng số byte mà IndexedDB và API Bộ nhớ đệm sử dụng, đồng thời cho phép tính toán dung lượng bộ nhớ còn lại gần đúng.

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

Bạn phải phát hiện lỗi vượt quá hạn mức (xem bên dưới). Trong một số trường hợp, hạn mức hiện có có thể vượt quá dung lượng bộ nhớ thực tế hiện có.

Kiểm tra

Trong quá trình phát triển, bạn có thể sử dụng DevTools của trình duyệt để kiểm tra các loại bộ nhớ khác nhau và xoá tất cả dữ liệu đã lưu trữ.

Chrome 88 đã thêm một tính năng mới cho phép bạn ghi đè hạn mức bộ nhớ của trang web trong Ngăn bộ nhớ. Tính năng này cho phép bạn mô phỏng nhiều thiết bị và kiểm thử hành vi của ứng dụng trong các trường hợp dung lượng ổ đĩa thấp. Chuyển đến Application (Ứng dụng) rồi chuyển đến Storage (Bộ nhớ), bật hộp đánh dấu Simulate custom storage quota (Mô phỏng hạn mức bộ nhớ tuỳ chỉnh) rồi nhập bất kỳ số hợp lệ nào để mô phỏng hạn mức bộ nhớ.

Trong quá trình viết hướng dẫn này, tôi đã viết một công cụ đơn giản để cố gắng sử dụng nhanh nhiều bộ nhớ nhất có thể. Đây là một cách nhanh chóng để thử nghiệm với nhiều cơ chế lưu trữ và xem điều gì sẽ xảy ra khi bạn sử dụng hết hạn mức.

Cách xử lý khi vượt quá hạn mức?

Bạn nên làm gì khi vượt quá hạn mức? Quan trọng nhất, bạn phải luôn phát hiện và xử lý lỗi ghi, cho dù đó là QuotaExceededError hay lỗi nào khác. Sau đó, tuỳ thuộc vào thiết kế ứng dụng, hãy quyết định cách xử lý. Ví dụ: xoá nội dung không được truy cập trong một thời gian dài, xoá dữ liệu dựa trên kích thước hoặc cung cấp cách để người dùng chọn nội dung họ muốn xoá.

Cả IndexedDB và API bộ nhớ đệm đều gửi một DOMError có tên là QuotaExceededError khi bạn đã vượt quá hạn mức hiện có.

IndexedDB

Nếu nguồn gốc đã vượt quá hạn mức, thì các nỗ lực ghi vào IndexedDB sẽ không thành công. Trình xử lý onabort() của giao dịch sẽ được gọi, truyền một sự kiện. Sự kiện này sẽ bao gồm DOMException trong thuộc tính lỗi. Việc kiểm tra lỗi name sẽ trả về QuotaExceededError.

const transaction = idb.transaction(['entries'], 'readwrite');
transaction.onabort = function(event) {
  const error = event.target.error; // DOMException
  if (error.name == 'QuotaExceededError') {
    // Fallback code goes here
  }
};

API Bộ nhớ đệm

Nếu nguồn gốc đã vượt quá hạn mức, thì các nỗ lực ghi vào API Bộ nhớ đệm sẽ bị từ chối bằng QuotaExceededError DOMException.

try {
  const cache = await caches.open('my-cache');
  await cache.add(new Request('/sample1.jpg'));
} catch (err) {
  if (error.name === 'QuotaExceededError') {
    // Fallback code goes here
  }
}

Cơ chế loại bỏ hoạt động như thế nào?

Bộ nhớ web được phân loại thành hai nhóm, "Tối đa" và "Bền vững". Trình duyệt có thể xoá bộ nhớ mà không làm gián đoạn người dùng, nhưng không bền đối với dữ liệu quan trọng hoặc dài hạn. Bộ nhớ cố định không tự động bị xoá khi bộ nhớ sắp hết. Người dùng cần xoá bộ nhớ này theo cách thủ công (thông qua các chế độ cài đặt của trình duyệt).

Theo mặc định, dữ liệu của một trang web (bao gồm cả IndexedDB, Cache API, v.v.) thuộc danh mục nỗ lực cao nhất, có nghĩa là trừ khi trang web yêu cầu lưu trữ liên tục, trình duyệt có thể tùy ý xoá dữ liệu trang web, ví dụ như khi thiết bị sắp hết dung lượng lưu trữ.

Chính sách trục xuất đối với nỗ lực cao nhất là:

  • Các trình duyệt dựa trên Chromium sẽ bắt đầu xoá dữ liệu khi trình duyệt hết dung lượng, trước tiên là xoá tất cả dữ liệu trang web từ nguồn được sử dụng ít nhất gần đây, sau đó là nguồn tiếp theo, cho đến khi trình duyệt không còn vượt quá giới hạn.
  • Firefox sẽ bắt đầu xoá dữ liệu khi dung lượng ổ đĩa trống đã đầy, trước tiên là xoá tất cả dữ liệu trang web từ nguồn được sử dụng gần đây nhất, sau đó là xoá dữ liệu từ nguồn tiếp theo, cho đến khi trình duyệt không còn vượt quá giới hạn.
  • Trước đây, Safari không xoá dữ liệu, nhưng gần đây đã triển khai một giới hạn mới là 7 ngày đối với tất cả bộ nhớ có thể ghi (xem bên dưới).

Kể từ iOS và iPadOS 13.4 và Safari 13.1 trên macOS, giới hạn 7 ngày cho tất cả bộ nhớ có thể ghi của tập lệnh, bao gồm cả IndexedDB, đăng ký trình chạy dịch vụ và API Bộ nhớ đệm. Điều này có nghĩa là Safari sẽ xoá tất cả nội dung khỏi bộ nhớ đệm sau 7 ngày sử dụng Safari nếu người dùng không tương tác với trang web. Chính sách loại bỏ này không áp dụng cho các PWA đã cài đặt đã được thêm vào màn hình chính. Hãy xem bài viết Chặn toàn bộ cookie của bên thứ ba và nhiều tính năng khác trên blog WebKit để biết thông tin chi tiết đầy đủ.

Bộ chứa lưu trữ

Ý tưởng chính của API Bộ chứa lưu trữ là cấp cho các trang web khả năng tạo nhiều bộ chứa lưu trữ, trong đó trình duyệt có thể chọn xoá từng bộ chứa một cách độc lập với các bộ chứa khác. Điều này cho phép nhà phát triển chỉ định mức độ ưu tiên loại bỏ để đảm bảo dữ liệu có giá trị nhất không bị xoá.

Phần thưởng: Lý do sử dụng trình bao bọc cho IndexedDB

IndexedDB là một API cấp thấp yêu cầu thiết lập đáng kể trước khi sử dụng, điều này có thể đặc biệt khó khăn khi lưu trữ dữ liệu có độ phức tạp thấp. Không giống như hầu hết các API hiện đại dựa trên lời hứa, API này dựa trên sự kiện. Trình bao bọc Promise như idb cho IndexedDB ẩn một số tính năng mạnh mẽ, nhưng quan trọng hơn là ẩn cơ chế phức tạp (ví dụ: giao dịch, phiên bản giản đồ) đi kèm với thư viện IndexedDB.

Phần thưởng: SQLite Wasm

Sau khi Web SQL không còn được dùng nữa và bị xoá khỏi Chrome, Google đã làm việc với các nhà bảo trì cơ sở dữ liệu SQLite phổ biến để cung cấp một giải pháp thay thế cho Web SQL dựa trên SQLite. Hãy đọc bài viết SQLite Wasm trên trình duyệt được Hệ thống tệp riêng tư gốc hỗ trợ để biết chi tiết về cách sử dụng công cụ này.

Kết luận

Thời gian lưu trữ bị hạn chế và nhắc người dùng lưu trữ ngày càng nhiều dữ liệu đã qua rồi. Các trang web có thể lưu trữ hiệu quả tất cả tài nguyên và dữ liệu cần thiết để chạy. Khi sử dụng StorageManager API, bạn có thể xác định dung lượng còn trống và dung lượng đã sử dụng. Và với bộ nhớ cố định, trừ phi người dùng xoá, bạn có thể bảo vệ bộ nhớ này khỏi bị loại bỏ.

Tài nguyên khác

Cảm ơn bạn!

Cảm ơn đặc biệt Jarryd Goodman, Phil Walton, Eiji Kitamura, Daniel Murphy, Darwin Huang, Josh Bell, Marijn Kruisselbrink và Victor Costan đã xem xét hướng dẫn này. Cảm ơn Eiji Kitamura, Addy Osmani và Marc Cohen đã viết các bài viết ban đầu mà bài viết này dựa trên đó. Eiji đã viết một công cụ hữu ích có tên là Trình vi phạm bộ nhớ trình duyệt. Công cụ này rất hữu ích trong việc xác thực hành vi hiện tại. Tính năng này cho phép bạn lưu trữ nhiều dữ liệu nhất có thể và xem giới hạn bộ nhớ trên trình duyệt. Cảm ơn François Beaufort đã tìm hiểu Safari để tìm ra các giới hạn bộ nhớ của trình duyệt này và cảm ơn Thomas Steiner đã thêm thông tin về hệ thống tệp riêng tư gốc, bộ lưu trữ, SQLite Wasm và nội dung cập nhật tổng thể vào năm 2024.

Hình ảnh chính là của Guillaume Bolduc trên Unsplash.