В прошлой статье мы разбирались с localStorage и sessionStorage — простыми браузерными хранилищами, которые хорошо подходят для небольших объёмов данных и несложного состояния приложения. Но что делать, если фронтенду нужно хранить не пару настроек, а тысячи объектов, работать с ними офлайн, быстро искать записи или сохранять файлы и другие данные, с которыми неудобно работать через Web Storage?
Здесь появляется IndexedDB — встроенный в браузер API для клиентского хранения больших объёмов данных. Это транзакционная система базы данных с объектной моделью: она позволяет хранить JavaScript-объекты, создавать для них индексы и выполнять операции в рамках транзакций. При этом IndexedDB умеет работать не только с обычными объектами, в ней можно хранить, например, Blob и File.
В отличие от Web Storage, IndexedDB не представляет данные как набор строковых значений «ключ → значение». Она предоставляет отдельные хранилища объектов, ключи и индексы, поэтому гораздо лучше подходит для приложений, которым нужно работать с большим количеством структурированных данных. Операции с IndexedDB выполняются асинхронно и не блокируют приложение.
Почему LocalStorage начинает мешать
Представим приложение для учёта товаров, которое должно работать даже без интернета. В нём нужно сохранить несколько тысяч товаров, а затем быстро находить нужный по идентификатору, категории или названию.
С localStorage технически можно сохранить массив:
Но дальше начинаются неудобства. Чтобы изменить один товар, приложение должно прочитать весь массив, распарсить JSON, найти нужный объект, изменить его, снова сериализовать весь массив и записать обратно.
IndexedDB устроена иначе. Объекты можно хранить по отдельности, а для часто используемых полей создавать индексы. Поэтому запрос к базе не требует каждый раз загружать и разбирать весь набор данных. Кроме того, IndexedDB работает асинхронно. Браузер не блокирует основной поток, пока приложение обращается к базе, что особенно важно для больших объёмов данных.
Как вообще устроена IndexedDB?
Если привыкли к реляционным базам данных, терминология сначала может показаться необычной. В IndexedDB есть база данных, внутри неё — object stores, которые можно условно сравнить с таблицами. В object store хранятся JavaScript-объекты, а для быстрого поиска можно создавать индексы.
Например, у приложения может быть база ShopDB, а внутри — object store products. Каждый товар хранится как отдельный объект:
Для id можно использовать ключ записи, а для category или других часто используемых полей создать индексы.
При этом IndexedDB — не реляционная база. Здесь нет привычных SQL-таблиц и JOIN. Данные организованы вокруг объектов и ключей, а запросы выполняются через API IndexedDB.
С чего начинается работа с базой
Сначала приложение открывает базу:
Число 1 — версия базы данных. И здесь появляется важный момент: IndexedDB имеет механизм миграций схемы.
Когда база создаётся впервые или её версия увеличивается, срабатывает onupgradeneeded:
Здесь мы создаём object store products, указываем, что поле id будет ключом записи, и создаём индекс по category. Именно в onupgradeneeded обычно изменяют структуру базы: создают новые object stores, индексы или выполняют миграции.
Это похоже на миграции схемы в обычной базе данных: если приложение перешло с версии 1 на версию 2 и ему понадобилось новое хранилище, изменение структуры выполняется именно во время обновления версии.
Добавляем данные
После успешного открытия базы можно начать работать с object store. Для этого создаётся транзакция:
Транзакция здесь не случайна. IndexedDB группирует операции чтения и изменения данных в транзакции, что позволяет браузеру контролировать их выполнение и целостность.
Для операций, которые изменяют данные, обычно используется режим readwrite. Для чтения достаточно readonly, который также является режимом по умолчанию.
Если объект с таким ключом уже существует, add() завершится ошибкой. Когда нужно создать новую запись или заменить существующую, используют put():
Это одно из важных различий API: add() предназначен для добавления новой записи, а put() позволяет добавить или обновить запись с указанным ключом.
Чтение данных
Получить конкретный объект по ключу можно через get():
Если записи с таким ключом нет, результатом будет undefined.
Но IndexedDB умеет искать не только по ключу. Для этого как раз пригодятся индексы. Например, если мы заранее создали индекс category, можно получить товары определённой категории:
Именно индексы делают IndexedDB гораздо удобнее для приложений, которым приходится регулярно искать данные по определённым полям.
А как удалить или изменить запись?
Для удаления используется delete():
Для обновления уже существующей записи можно использовать put():
Таким образом, базовый CRUD выглядит вполне знакомо: создать, прочитать, обновить и удалить данные. Но вместо SQL-запросов здесь используются методы IndexedDB API, object stores, ключи и индексы.
Почему IndexedDB асинхронная?
Это одно из принципиальных отличий от localStorage.
Операции IndexedDB возвращают специальные объекты IDBRequest, а результат приходит через события или современные обёртки над API. Например:
Такой API исторически выглядит непривычно, особенно если привыкнуть к async/await.
Поэтому в современных приложениях разработчики нередко используют библиотеки-обёртки, например, Dexie, которые предоставляют более удобный интерфейс поверх IndexedDB. Но сама база при этом остаётся IndexedDB, библиотека лишь упрощает работу с её API.
Транзакции — одна из ключевых частей IndexedDB
Представим, что пользователь оформляет заказ. Нужно добавить сам заказ и одновременно изменить несколько связанных записей: например, уменьшить количество товаров на складе.
Такие операции можно выполнить в рамках одной транзакции:
Идея здесь та же, что и у транзакций в других базах данных: несколько операций рассматриваются как единое изменение. Если операция не может быть корректно завершена, транзакция может быть отменена, вместо того чтобы оставить базу в промежуточном состоянии.
Поэтому транзакции особенно важны, когда изменение одного объекта должно быть согласовано с изменением другого.
Где IndexedDB особенно полезна?
Один из главных сценариев — offline-first приложения. Например, пользователь может открыть приложение без интернета, увидеть ранее загруженные данные, продолжить работу, а после восстановления соединения изменения будут синхронизированы с сервером.
IndexedDB также используют для хранения больших объёмов структурированных данных, кеширования данных приложения, локальных каталогов, сообщений, черновиков и других данных, с которыми неудобно работать через Web Storage.
При этом IndexedDB не обязательно должна заменять серверную базу данных. В большинстве веб-приложений сервер остаётся источником истины, а IndexedDB используется как локальный слой хранения и кеширования.
Можно ли хранить файлы?
Да. IndexedDB может хранить не только обычные JavaScript-объекты, но и некоторые другие структурированные данные, включая Blob и File. Это позволяет, например, реализовать приложение, которое временно сохраняет загруженные пользователем файлы для последующей обработки или отправки.
! Но здесь тоже не стоит воспринимать IndexedDB как универсальную файловую систему. Для действительно больших объёмов данных нужно учитывать ограничения браузера, доступное дисковое пространство и политики конкретной среды.
А что с объёмом хранилища?
В отличие от Web Storage, IndexedDB рассчитана на существенно более серьёзные объёмы данных. Но это не означает, что приложение получает бесконечное дисковое пространство.
Доступный объём и поведение браузера зависят от браузера, устройства, свободного места и других условий. Кроме того, браузер может применять собственные политики хранения и очистки данных. Поэтому IndexedDB подходит для больших клиентских данных гораздо лучше, чем localStorage, но приложение всё равно должно быть готово к ошибкам хранения и к тому, что локальные данные не являются вечными.
А что насчёт безопасности?
Как и localStorage, IndexedDB не стоит воспринимать как защищённое хранилище секретов.
Данные IndexedDB доступны JavaScript-коду страницы в рамках соответствующего origin. Поэтому XSS-уязвимость может поставить под угрозу и данные, которые приложение хранит в IndexedDB. То, что данные не лежат в обычном текстовом поле localStorage, не делает их автоматически безопасными.
Для чувствительной информации нужно отдельно продумывать модель безопасности, права доступа и последствия компрометации данных.
IndexedDB или LocalStorage?
Здесь не стоит выбирать «победителя». Эти инструменты рассчитаны на разные задачи.
localStorage удобен, когда нужно сохранить небольшое количество простых данных: тему, язык, настройки интерфейса или другое состояние, представленное несколькими ключами.
IndexedDB имеет смысл, когда приложение работает со структурированными данными, большими объёмами информации, индексами, транзакциями или должно полноценно работать офлайн.
Можно представить разницу так: localStorage — это небольшой ящик, куда удобно положить несколько нужных вещей. IndexedDB — уже организованное хранилище, где есть отдельные секции, ключи, индексы и правила работы с данными.
При этом переходить на IndexedDB только потому, что она «мощнее», тоже не стоит. Если приложению нужно сохранить одну настройку пользователя, полноценная база данных будет избыточной.
IndexedDB стоит рассматривать как локальный слой хранения, который дополняет серверную часть приложения. Она особенно полезна там, где клиенту нужно работать с большим объёмом данных, сохранять их между сессиями или поддерживать работу без постоянного подключения к сети.
При этом IndexedDB не заменяет backend-базу: сервер остаётся источником основной версии данных, а браузер может хранить их локальную копию для более быстрой и устойчивой работы приложения.
Хотите узнать больше? Изучите другие статьи из раздела: