Có nhiều lựa chọn để lưu trữ dữ liệu trong trình duyệt. Loại 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 tồn tại khi đang di chuyển. Đó là lý do tại sao hỗ trợ khi không có mạng 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ó thể cải thiện đáng kể trải nghiệm người dùng. Có nhiều cách để lưu vào bộ nhớ đệm 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.). Nhưng đâu là giải pháp tốt nhất? Bạn có thể lưu trữ bao nhiêu? Làm cách nào để ngăn chặn việc này?
Tôi nên sử dụng loại nào?
Sau đây là đề xuất chung về cách lưu trữ tài nguyên:
- Đối với các tài nguyên mạng cần thiết để tải ứng dụng, hãy sử dụng Cache Storage API (một phần của worker dịch vụ).
- Đối với nội dung dựa trên tệp, hãy sử dụng Origin Private File System (OPFS).
- Đối với dữ liệu khác, hãy dùng IndexedDB (với trình bao bọc lời hứa).
IndexedDB, OPFS và Cache Storage API được hỗ trợ trong mọi trình duyệt hiện đại.
Chúng 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 dịch vụ). Bạn có thể truy cập vào các đối tượng này từ đối tượng window, web worker và service worker, nhờ đó bạn có thể sử dụng chúng ở bất kỳ đâu trong mã của mình.
Còn các cơ chế lưu trữ khác thì sao?
Có một số cơ chế lưu trữ khác trong trình duyệt, nhưng chúng có hạn chế về việc sử dụng 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à có phạm vi trong suốt thời gian tồn tại của thẻ. Bạn có thể dùng API này để lưu trữ một lượng nhỏ thông tin cụ thể về phiên, chẳng hạn như khoá IndexedDB. Bạn nên thận trọng khi sử dụng vì phương thức này có tính đồng bộ và sẽ chặn luồng chính. Kích thước của tệp này bị giới hạn ở khoảng 5 MB và chỉ có thể chứa các chuỗi. Vì chỉ dành riêng cho thẻ, nên bạn không thể truy cập vào thẻ này từ các worker trên web hoặc service worker.
Bạn nên tránh dùng LocalStorage vì tính năng này có tính đồng bộ và sẽ chặn luồng chính. Kích thước của tệp này bị giới hạn ở khoảng 5 MB và chỉ có thể chứa các chuỗi. Không thể truy cập LocalStorage từ trình chạy web hoặc trình chạy dịch vụ.
Cookie có nhiều mục đích sử dụ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ỳ dữ liệu nào lớn hơn một lượng nhỏ sẽ làm tăng đáng kể kích thước của mọi yêu cầu web. Chúng là đồng bộ và không thể truy cập từ các worker trên web. Giống như LocalStorage và SessionStorage, cookie chỉ giới hạn ở các chuỗi.
File System Access API được thiết kế để cho phép người dùng đọc và chỉnh sửa tệp trên hệ thống tệp cục bộ. Người dùng phải cấp quyền trước khi 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 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 vào tệp.
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 được cách ly. Mặc dù không đồng bộ, nhưng bạn không nên dùng vì chỉ có trong các trình duyệt dựa trên Chromium.
Tôi có thể lưu trữ bao nhiêu?
Nói 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 hoặc hơn. Các cách triển khai của trình duyệt sẽ 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 đa 80% tổng dung lượng ổ đĩa. Một nguồn có thể sử dụng tối đa 60% tổng dung lượng ổ đĩa. Bạn có thể sử dụng StorageManager API để 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 giảm lượng bộ nhớ mà một nguồn có thể sử dụng xuống khoảng 5% tổng dung lượng ổ đĩa.
- Nếu người dùng đã bật chế độ "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 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.comvàfoo.bar.example.com) có thể sử dụng tối đa 2 GB. Bạn có thể dùng StorageManager API để xác định dung lượng còn trống. - Safari (cả trên máy tính và thiết bị di động) dường 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 gia tăng 200 MB. Tôi không tìm được tài liệu chính thức nào về vấn đề này.
- Nếu được thêm vào màn hình chính trên Safari di động, PWA sẽ tạo một vùng chứa lưu trữ mới và không có dữ liệu nào được chia sẻ giữa PWA và Safari di động. Sau khi đạt đến hạn mức cho một PWA đã cài đặt, có vẻ 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á một 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 hơn 50 MB, trình duyệt sẽ nhắc người dùng cho phép nguồn gốc đó 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 tối đa hạn mức được phân bổ. Ngoại lệ có vẻ là Safari, trình duyệt này sẽ nhắc khi hạn mức bộ nhớ bị vượt quá, yêu cầu cấp quyền tăng hạn mức được phân bổ. Nếu một nguồn cố gắng sử dụng nhiều hơn hạn mức được phân bổ, thì các lần cố gắng 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ể dùng StorageManager API để xác định lượng bộ nhớ mà nguồn gốc có thể sử dụng và lượng bộ nhớ mà nguồn gốc đang dùng. API này báo cáo tổng số byte mà IndexedDB và Cache API sử dụng, đồng thời cho phép tính toán dung lượng lưu trữ 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 nắm bắt các 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 có thể vượt quá dung lượng lưu trữ thực tế.
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ữ.
Một tính năng mới đã được thêm vào Chrome 88, cho phép bạn ghi đè hạn mức lưu trữ của trang web trong ngăn Lưu trữ. 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 có ít dung lượng ổ đĩa. Chuyển đến Application (Ứng dụng) rồi chọn Storage (Bộ nhớ), đánh dấu vào hộp Simulate custom storage quota (Mô phỏng hạn mức bộ nhớ tuỳ chỉnh) rồi nhập một số hợp lệ bất kỳ để mô phỏng hạn mức bộ nhớ.
Trong khi viết hướng dẫn này, tôi đã viết một công cụ đơn giản để cố gắng nhanh chóng sử dụng nhiều bộ nhớ nhất có thể. Đây là một cách nhanh chóng để thử nghiệm các cơ chế lưu trữ khác nhau 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 là bạn phải luôn phát hiện và xử lý các 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 đã lâu không được truy cập, xoá dữ liệu dựa trên kích thước hoặc cung cấp cho người dùng cách chọn nội dung họ muốn xoá.
Cả IndexedDB và Cache API đề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 lần thử 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 một DOMException trong thuộc tính lỗi. 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
}
};
Cache API
Nếu nguồn gốc đã vượt quá hạn mức, các lần thử ghi vào Cache API 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ốt nhất có thể" và "Duy trì". Nỗ lực tối đa có nghĩa là trình duyệt có thể xoá bộ nhớ mà không làm gián đoạn người dùng, nhưng ít bền hơn đối với dữ liệu quan trọng hoặc dài hạn. Bộ nhớ liên tục sẽ không tự động được 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 IndexedDB, Cache API, v.v.) thuộc danh mục nỗ lực tối đa. Điều này có nghĩa là trừ phi một trang web đã yêu cầu bộ nhớ liên tục, nếu không, trình duyệt có thể loại bỏ dữ liệu trang web theo ý mình, chẳng hạn như khi bộ nhớ thiết bị sắp hết.
Chính sách loại bỏ cho chế độ tối ưu nhất là:
- Các trình duyệt dựa trên Chromium sẽ bắt đầu loại bỏ dữ liệu khi trình duyệt hết dung lượng, xoá tất cả dữ liệu trang web khỏi nguồn gốc ít được dùng gần đây nhất trước, sau đó đến nguồn gốc tiếp theo, cho đến khi trình duyệt không vượt quá giới hạn nữa.
- Firefox sẽ bắt đầu loại bỏ dữ liệu khi hết dung lượng ổ đĩa trống, trước tiên là xoá tất cả dữ liệu trang web khỏi nguồn gốc được sử dụng gần đây nhất, sau đó là nguồn gốc 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 loại bỏ 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 cũng như Safari 13.1 trên macOS, tất cả bộ nhớ có thể ghi của tập lệnh đều có giới hạn là 7 ngày, bao gồm cả IndexedDB, đăng ký worker dịch vụ và Cache API. Đ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 hoàn toàn cookie của bên thứ ba và hơn thế nữa trên blog WebKit để biết thông tin chi tiết đầy đủ.
Bộ chứa lưu trữ
Ý tưởng cốt lõi của Storage Buckets API là cấp cho các trang web khả năng tạo nhiều vùng lưu trữ, trong đó trình duyệt có thể chọn xoá từng vùng lưu trữ độc lập với các vùng lưu trữ 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: Tại sao nên dùng trình bao bọc cho IndexedDB
IndexedDB là một API cấp thấp đòi hỏi phải 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 promise, API này dựa trên sự kiện. Các trình bao bọc Promise như idb cho IndexedDB sẽ ẩ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, lập phiên bản lược đồ) đi kèm với thư viện IndexedDB.
Quảng cáo thêm về tính năng: SQLite Wasm
Sau khi Web SQL không còn được dùng nữa và bị xoá khỏi Chrome, Google đã hợp tác với những người duy 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 trong trình duyệt được hỗ trợ bởi Origin Private File System để biết thông tin chi tiết về cách sử dụng.
Kết luận
Đã qua rồi cái thời bộ nhớ hạn chế và nhắc người dùng lưu trữ ngày càng nhiều dữ liệu. 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ới bộ nhớ liên tục, bạn có thể bảo vệ bộ nhớ này khỏi bị loại bỏ, trừ phi người dùng xoá bộ nhớ đó.
Tài nguyên khác
Cảm ơn bạn!
Xin chân thành cảm ơn 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 gốc 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à Browser Storage Abuser (Công cụ khai thác bộ nhớ trình duyệt) để 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 lưu trữ trên trình duyệt. Cảm ơn François Beaufort đã tìm hiểu về Safari để xác định giới hạn lưu trữ của trình duyệt này, cũng như cảm ơn Thomas Steiner đã bổ sung thông tin về hệ thống tệp riêng tư gốc, các vùng lưu trữ, SQLite Wasm và nội dung cập nhật tổng thể vào năm 2024.