Как использовать IndexedDB в браузере: руководство и лучшие практики
Что такое IndexedDB
IndexedDB — это клиентская NoSQL-база данных, встроенная в веб-браузеры, которая позволяет сохранять и извлекать структурированные объекты (ключ–значение) на стороне пользователя. Ключевая особенность — поддержка транзакций, индексов и больших объёмов данных, а также работа в офлайне.
Краткое определение: IndexedDB — асинхронная, транзакционная база данных в браузере для хранения объектов и индексированных запросов.
Важно: IndexedDB — не замена серверной БД. Она хранит данные локально у пользователя и управляется в пределах домена/происхождения (origin).
Преимущества и когда не использовать
Преимущества:
- Поддержка больших объёмов данных и бинарных объектов (Blob, ArrayBuffer).
- Асинхронный API, не блокирует главный поток (в отличие от sync localStorage в старых реализациях).
- Транзакции гарантируют согласованность при нескольких операциях.
- Индексы позволяют эффективнее выполнять поиск и сортировку.
Когда не использовать IndexedDB:
- Для простых, коротких настроек или токенов, где достаточно localStorage или sessionStorage.
- Если требуется синхронный доступ к данным из нескольких вкладок без периодической синхронизации (в этом случае дополнительно нужны механизмы синхронизации).
- Если данные должны быть централизованно доступны и контролироваться на сервере — храните их на бэкенде.
Основная терминология
- Объектное хранилище (object store): контейнер для объектов, аналог таблицы в реляционных БД.
- Ключ (key) и путь ключа (keyPath): уникальный идентификатор записи в хранилище.
- Транзакция: атомарная группа операций чтения/записи.
- Индекс: вспомогательная структура для поиска по полям объекта.
Настройка базы данных
Чтобы создать или открыть базу, используйте indexedDB.open(name, version). Этот вызов возвращает объект IDBOpenDBRequest, который генерирует события success, error и upgradeneeded.
Пример создания запроса на открытие:
const openRequest = indexedDB.open("usersdb", 1);Обработка событий:
onsuccessпроисходит, когда база успешно открыта; доступ к DB черезevent.target.result.onerrorсрабатывает при ошибках открытия.onupgradeneededсрабатывает при первом создании базы или повышении версии — место для создания объектных хранилищ и индексов.
Пример обработчика success:
openRequest.onsuccess = function (event) {
const db = event.target.result;
console.log("Database created", db);
};Пример обработчика onupgradeneeded и создания объектного хранилища:
openRequest.onupgradeneeded = function (event) {
const db = event.target.result;
// Create an object store
const userObjectStore = db.createObjectStore("userStore", {
keyPath: "id",
autoIncrement: true,
});
// Create indexes
userObjectStore.createIndex("name", "name", { unique: false });
userObjectStore.createIndex("email", "email", { unique: true });
};Советы:
- Всегда планируйте схему и версии заранее — частые обновления версии приводят к миграциям.
- Обработчик
onupgradeneededвыполняется в контексте транзакции на схему, поэтому в нём безопасно менять структуру хранилищ.
Создание объектного хранилища и индексов
Объектное хранилище — эквивалент таблицы, где хранятся объекты JavaScript. При создании указывайте keyPath или autoIncrement:
keyPath: имя поля, которое будет ключом.autoIncrement: true: браузер сам будет генерировать числовые ключи.
Индексы создаются методом createIndex(name, keyPath, options). Индекс ускоряет поиск по полям и может быть уникальным (unique: true).
Пример создания индексов:
userObjectStore.createIndex("name", "name", { unique: false });
userObjectStore.createIndex("email", "email", { unique: true });Замечание: индексы нужно планировать с учётом пользовательских сценариев поиска и фильтрации.
Транзакции и режимы доступа
Транзакция группирует операции и обеспечивает их атомарность. Создаётся вызовом db.transaction(storeNames, mode), где mode — readonly или readwrite.
Пример создания транзакции и добавления данных:
const addUserData = (userData, db) => {
// Open a transaction
const transaction = db.transaction("userStore", "readwrite");
// Add data to the object store
const userObjectStore = transaction.objectStore("userStore");
// Make a request to add userData
const request = userObjectStore.add(userData);
// Handle a success event
request.onsuccess = function (event) {
//...
};
// Handle an error
request.onerror = function (event) {
//...
};
};Важно:
- Транзакция автоматически закрывается после завершения всех запросов; не полагайтесь на долгоживущие транзакции.
- Ошибка в одной операции может откатить все изменения в транзакции.
CRUD: добавление, чтение, обновление и удаление
Добавление (add): objectStore.add(value) — создаёт новую запись и выдаёт ошибку, если ключ уже существует.
Чтение: get(key) возвращает один объект по ключу; getAll() возвращает все записи или совпадающие по заданному диапазону.
Пример чтения по ключу:
const getUserData = (id, db) => {
const transaction = db.transaction("userStore", "readonly");
const userObjectStore = transaction.objectStore("userStore");
// Make a request to get the data
const request = userObjectStore.get(id);
request.onsuccess = function (event) {
console.log(request.result);
};
request.onerror = function (event) {
// Handle error
};
};Обновление: получите объект через get(), измените поля и сохраните через put() — put заменяет существующую запись или создаёт новую с указанным ключом.
const updateUserData = (id, userData, db) => {
const transaction = db.transaction("userStore", "readwrite");
const userObjectStore = transaction.objectStore("userStore");
// Make a request to get the data
const getRequest = userObjectStore.get(id);
// Handle a success event
getRequest.onsuccess = function (event) {
// Get the old user data
const user = event.target.result;
// Update the user data
user.name = userData.name;
user.email = userData.email;
// Make a request to update the data
const putRequest = userObjectStore.put(user);
putRequest.onsuccess = function (event) {
// Handle success
};
putRequest.onerror = function (event) {
// Handle error
};
};
getRequest.onerror = function (event) {
// Handle error
};
};Удаление: objectStore.delete(key) удаляет запись по ключу.
const deleteUserData = (id, db) => {
const transaction = db.transaction("userStore", "readwrite");
const userObjectStore = transaction.objectStore("userStore");
// Make a request to delete the data
const request = userObjectStore.delete(id);
request.onsuccess = function (event) {
// Handle success
};
request.onerror = function (event) {
// Handle error
};
};Шаблоны и лучшие практики
- Всегда проверяйте, что
dbоткрыт и доступен перед операциями. - Используйте
readwriteтолько когда нужно — это уменьшит конкуренцию и ускорит параллельные чтения. - Обрабатывайте
onerrorиonblockedсобытия при обновлении версии базы (blocked означает, что другая вкладка всё ещё держит старую версию). - Вставляйте большие Blob и бинарные данные осторожно; для крупных файлов лучше использовать файловые API или серверное хранение.
- Используйте индексы для часто используемых полей поиска и сортировки.
Производительность и ограничения
- IndexedDB оптимизирован для больших объёмов данных и может хранить значительно больше, чем localStorage, но точный лимит зависит от браузера и настроек пользователя.
- Пакетируйте операции в транзакции по возможностям, чтобы уменьшить количество открытий/закрытий транзакций.
- Для массовой загрузки данных используйте стратегию порций (batching) и энергосберегающие паузы, чтобы не блокировать UI.
Миграция с localStorage на IndexedDB
Когда переходить:
- Когда объём данных растёт и требуется индексированный поиск.
- Когда нужны транзакции и гарантии целостности.
Мини-методика миграции:
- Создайте структуру в IndexedDB и занесите схему в
onupgradeneeded. - При старте приложения проверяйте наличие данных в localStorage.
- Вставляйте данные из localStorage в IndexedDB пакетами и отмечайте флаг миграции.
- После успешной миграции очищайте localStorage или оставляйте как резервную копию по версии.
Тестовые критерии миграции:
- Все ключи и значения перенесены.
- Индексы доступны и корректны.
- Производительность не упала критически.
Безопасность и конфиденциальность
- Данные в IndexedDB доступны только из того же происхождения (origin). Кросс-доменные запросы не допускаются.
- Не храните критичные секреты (пароли в открытом виде, токены с длительным сроком) без шифрования. При необходимости шифруйте данные на клиенте перед сохранением.
- Учитывайте требования конфиденциальности и локальные законы (например, GDPR) при долговременном хранении персональных данных: давайте возможность удаления и экспорта данных пользователю.
- Всегда валидируйте данные перед вставкой, особенно если они приходят из внешних источников.
Отладка и распространённые ошибки
- “QuotaExceededError”: Обычно возникает, если пользователь исчерпал выделенное место или браузер ограничил запись. Решения: сжатие данных, удаление неактуальных данных, запрос у пользователя действия.
- “TransactionInactiveError”: Возникает, если попытаться использовать транзакцию после её завершения. Всегда выполняйте операции в пределах времени жизни транзакции.
onblockedпри обновлении версии: означет, что другая вкладка не закрыла соединение. Покажите пользователю инструкцию закрыть другие вкладки.
Тесты и критерии приёмки
Примеры тест-кейсов:
- Создание базы и объектного хранилища: при открытии базы создаются указанные сторы и индексы.
- CRUD базовый сценарий: добавить → получить → обновить → удалить, ожидаемый результат на каждом шаге.
- Транзакционная целостность: при намеренно вызванной ошибке внутри транзакции все изменения откатываются.
- Миграция: данные из localStorage корректно перенесены, флаг миграции установлен.
Критерии приёмки:
- Все основные сценарии CRUD проходят без ошибок.
- Индексы возвращают ожидаемые результаты при поиске.
- Приложение обрабатывает ошибки квоты и блокировки без падения.
Роли и контрольный список внедрения
Для разработчика:
- Спроектировать схему и версионирование базы.
- Реализовать обработчики
onsuccess,onerror,onupgradeneeded. - Добавить логику миграции и бэкап/очистку localStorage.
Для QA:
- Проверить сценарии CRUD и транзакции.
- Тестировать обновления версии и блокировки между вкладками.
- Протестировать поведение при ограниченной квоте диска.
Для продакт-менеджера:
- Оценить необходимость IndexedDB vs localStorage.
- Утвердить правила удерживания данных и сроки хранения.
Альтернативы и когда выбирать их
- localStorage: простые пары ключ–значение, синхронный API, ограничен объёмом и функциональностью.
- Cache API + Service Workers: лучше для кеширования HTTP-ресурсов и офлайн-страниц.
- WebSQL (устаревший): не поддерживается во многих браузерах.
- Серверное хранение: для централизованного доступа и бэкапа данных.
Выбор зависит от сценария: для сложных локальных структур и офлайна — IndexedDB; для простых настроек — localStorage; для кеша сетевых ответов — Cache API.
Совместимость и миграционные заметки
IndexedDB поддерживается в большинстве современных браузеров, но поведение квот и внутренних оптимизаций может отличаться. При обновлении версии базы:
- Обрабатывайте
onblocked, чтобы информировать пользователя о необходимости закрыть другие вкладки. - Выполняйте изменения схемы последовательно: миграция должна быть обратимо-предсказуемой.
Заключение
IndexedDB — мощный инструмент для клиентского хранения крупных и структурированных данных. Он обеспечивает транзакционность, индексацию и офлайн-доступ. Тем не менее, его следует использовать там, где это оправдано архитектурно: когда данные превышают возможности localStorage или требуется сложный поиск и согласованность.
Важно:
- Планируйте схемы и версионирование заранее.
- Обрабатывайте ошибки квоты и блокировок.
- Учитывайте требования безопасности и законодательства при хранении пользовательских данных.
Короткая шпаргалка для старта:
- Откройте базу:
indexedDB.open(name, version). - В
onupgradeneededсоздайте object stores и индексы. - В
onsuccessполучитеdbи выполняйте транзакции черезdb.transaction. - Для записи используйте
add()/put(), для чтения —get()/getAll(), для удаления —delete().
Важно: если приложение использует личные данные, добавьте на страницу настройки для экспорта и удаления данных по требованию пользователя.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone