IndexedDB в JavaScript

10 сентября 2026
Автор

Антон Ларичев

Что такое IndexedDB

IndexedDB — это низкоуровневый браузерный API для хранения значительных объёмов структурированных данных, включая файлы и блобы. В отличие от localStorage, который хранит только строки и ограничен примерно 5 МБ, IndexedDB позволяет работать с полноценными JavaScript-объектами, поддерживает транзакции, индексы и асинхронный доступ.

Основные характеристики:

  • Хранит пары ключ-значение, где значения — произвольные JavaScript-объекты
  • Поддерживает транзакции для атомарных операций
  • Работает асинхронно и не блокирует поток UI
  • Поддерживает индексы для быстрого поиска по любому полю
  • Лимит хранилища зависит от браузера и устройства (обычно от 50 МБ до нескольких ГБ)

Курс по теме

Основы JavaScript — курс

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Основные концепции

База данных

Каждый источник (origin) может иметь несколько баз данных. База данных идентифицируется именем и версией. При изменении структуры базы версию увеличивают.

Хранилище объектов

Аналог таблицы в реляционной базе данных. Каждое хранилище содержит набор записей, доступных по ключу.

Транзакция

Все операции выполняются в рамках транзакции. Транзакция обеспечивает атомарность: если одна операция завершится ошибкой, все изменения в рамках транзакции будут отменены.

Индекс

Позволяет эффективно искать записи по любому полю, а не только по первичному ключу.

Курсор

Механизм для поочерёдной итерации по записям хранилища с возможностью фильтрации.

Открытие базы данных

Вся работа с IndexedDB начинается с открытия (или создания) базы данных:

const request = indexedDB.open('myDatabase', 1);

request.onerror = (event) => {
  console.error('Ошибка открытия базы данных:', event.target.error);
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('База данных открыта успешно');
};

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  const usersStore = db.createObjectStore('users', { keyPath: 'id' });
  usersStore.createIndex('name', 'name', { unique: false });
  usersStore.createIndex('email', 'email', { unique: true });
};

Событие onupgradeneeded срабатывает при первом создании базы данных или при увеличении её версии. Это единственное место, где разрешено создавать и изменять хранилища объектов и индексы.

Добавление данных

Для записи данных нужна транзакция в режиме readwrite:

function addUser(db, user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.add(user);

    request.onsuccess = () => {
      resolve(request.result); // возвращает ключ добавленной записи
    };

    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// Использование
addUser(db, { id: 1, name: 'Иван Петров', email: 'ivan@example.com', age: 30 });
addUser(db, { id: 2, name: 'Мария Сидорова', email: 'maria@example.com', age: 25 });

Метод add выбросит ошибку, если запись с таким ключом уже существует. Для обновления существующей записи используйте put.

Чтение данных

Получение записи по ключу

function getUserById(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.get(id);

    request.onsuccess = () => {
      resolve(request.result); // undefined, если запись не найдена
    };

    request.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

getUserById(db, 1).then(user => {
  console.log('Найден пользователь:', user);
});

Получение всех записей

function getAllUsers(db) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.getAll();

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

Обновление данных

Метод put добавляет запись или полностью заменяет её, если запись с таким ключом уже существует:

function updateUser(db, user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.put(user);

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

updateUser(db, { id: 1, name: 'Иван Петров', email: 'ivan@example.com', age: 31 });

Удаление данных

function deleteUser(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const store = transaction.objectStore('users');
    const request = store.delete(id);

    request.onsuccess = () => resolve();
    request.onerror = (event) => reject(event.target.error);
  });
}

Работа с индексами

Индексы позволяют искать записи не только по первичному ключу, но и по любому другому полю:

function getUserByEmail(db, email) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('email');
    const request = index.get(email);

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

getUserByEmail(db, 'ivan@example.com').then(user => {
  console.log('Пользователь по email:', user);
});

Работа с курсором

Курсор позволяет итерировать по всем записям хранилища и применять произвольную фильтрацию:

function getUsersOlderThan(db, minAge) {
  return new Promise((resolve, reject) => {
    const users = [];
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.openCursor();

    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        if (cursor.value.age >= minAge) {
          users.push(cursor.value);
        }
        cursor.continue();
      } else {
        resolve(users);
      }
    };

    request.onerror = (event) => reject(event.target.error);
  });
}

Диапазоны ключей с IDBKeyRange

Для поиска по диапазону значений используется IDBKeyRange:

// Пользователи с возрастом от 20 до 35 лет (нужен индекс по полю age)
function getUsersByAgeRange(db, minAge, maxAge) {
  return new Promise((resolve, reject) => {
    const users = [];
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const index = store.index('age');

    // bound(lower, upper, lowerOpen, upperOpen)
    const range = IDBKeyRange.bound(minAge, maxAge);
    const request = index.openCursor(range);

    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        users.push(cursor.value);
        cursor.continue();
      } else {
        resolve(users);
      }
    };

    request.onerror = (event) => reject(event.target.error);
  });
}

Доступные методы IDBKeyRange:

  • IDBKeyRange.only(value) — строгое совпадение
  • IDBKeyRange.lowerBound(value, open?) — значения больше (или равные)
  • IDBKeyRange.upperBound(value, open?) — значения меньше (или равные)
  • IDBKeyRange.bound(lower, upper, lowerOpen?, upperOpen?) — значения в диапазоне

Promise-обёртка над IndexedDB

Нативный API IndexedDB основан на событиях, что неудобно при работе с современным асинхронным кодом. Создадим универсальную обёртку:

class DatabaseService {
  constructor(dbName, version = 1) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
  }

  open(stores) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        stores.forEach(({ name, keyPath, autoIncrement = false, indexes = [] }) => {
          if (!db.objectStoreNames.contains(name)) {
            const store = db.createObjectStore(name, { keyPath, autoIncrement });
            indexes.forEach(({ name: idxName, keyPath: idxKey, unique = false }) => {
              store.createIndex(idxName, idxKey, { unique });
            });
          }
        });
      };

      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve(this);
      };

      request.onerror = (event) => reject(event.target.error);
    });
  }

  add(storeName, item) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction([storeName], 'readwrite');
      const store = tx.objectStore(storeName);
      const req = store.add(item);
      req.onsuccess = () => resolve(req.result);
      req.onerror = (e) => reject(e.target.error);
    });
  }

  get(storeName, key) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction([storeName], 'readonly');
      const store = tx.objectStore(storeName);
      const req = store.get(key);
      req.onsuccess = () => resolve(req.result);
      req.onerror = (e) => reject(e.target.error);
    });
  }

  getAll(storeName) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction([storeName], 'readonly');
      const store = tx.objectStore(storeName);
      const req = store.getAll();
      req.onsuccess = () => resolve(req.result);
      req.onerror = (e) => reject(e.target.error);
    });
  }

  put(storeName, item) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction([storeName], 'readwrite');
      const store = tx.objectStore(storeName);
      const req = store.put(item);
      req.onsuccess = () => resolve(req.result);
      req.onerror = (e) => reject(e.target.error);
    });
  }

  delete(storeName, key) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction([storeName], 'readwrite');
      const store = tx.objectStore(storeName);
      const req = store.delete(key);
      req.onsuccess = () => resolve();
      req.onerror = (e) => reject(e.target.error);
    });
  }
}

// Использование
const dbService = new DatabaseService('appDB', 1);

await dbService.open([
  {
    name: 'users',
    keyPath: 'id',
    indexes: [
      { name: 'email', keyPath: 'email', unique: true },
      { name: 'name', keyPath: 'name', unique: false }
    ]
  }
]);

await dbService.add('users', { id: 1, name: 'Алексей', email: 'alex@example.com' });
const user = await dbService.get('users', 1);
console.log(user); // { id: 1, name: 'Алексей', email: 'alex@example.com' }

Миграции и версионирование

При изменении структуры базы данных нужно увеличивать версию и обрабатывать все миграции инкрементально:

const request = indexedDB.open('myDatabase', 3);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const oldVersion = event.oldVersion;
  const transaction = event.target.transaction;

  if (oldVersion < 1) {
    // Первоначальная схема
    const store = db.createObjectStore('users', { keyPath: 'id' });
    store.createIndex('email', 'email', { unique: true });
  }

  if (oldVersion < 2) {
    // Добавляем индекс по имени
    const store = transaction.objectStore('users');
    store.createIndex('name', 'name', { unique: false });
  }

  if (oldVersion < 3) {
    // Добавляем новое хранилище для продуктов
    const productsStore = db.createObjectStore('products', { keyPath: 'id', autoIncrement: true });
    productsStore.createIndex('category', 'category', { unique: false });
  }
};

Порядок важен: каждый блок if (oldVersion < N) применяется только если база данных ещё не достигла этой версии. Так пользователь, обновляющийся с версии 1 сразу до версии 3, пройдёт все промежуточные миграции.

Практический пример: офлайн-список задач

class TodoApp {
  constructor() {
    this.db = null;
  }

  async init() {
    this.db = await this.openDB();
  }

  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('todoDB', 1);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        const store = db.createObjectStore('todos', {
          keyPath: 'id',
          autoIncrement: true
        });
        store.createIndex('completed', 'completed', { unique: false });
      };

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

  addTodo(text) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction(['todos'], 'readwrite');
      const store = tx.objectStore('todos');
      const request = store.add({
        text,
        completed: false,
        createdAt: Date.now()
      });
      request.onsuccess = () => resolve(request.result);
      request.onerror = (e) => reject(e.target.error);
    });
  }

  toggleTodo(id) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction(['todos'], 'readwrite');
      const store = tx.objectStore('todos');

      const getReq = store.get(id);
      getReq.onsuccess = () => {
        const todo = getReq.result;
        todo.completed = !todo.completed;
        const putReq = store.put(todo);
        putReq.onsuccess = () => resolve(todo);
        putReq.onerror = (e) => reject(e.target.error);
      };
      getReq.onerror = (e) => reject(e.target.error);
    });
  }

  getActiveTodos() {
    return new Promise((resolve, reject) => {
      const todos = [];
      const tx = this.db.transaction(['todos'], 'readonly');
      const store = tx.objectStore('todos');
      const index = store.index('completed');
      const request = index.openCursor(IDBKeyRange.only(false));

      request.onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
          todos.push(cursor.value);
          cursor.continue();
        } else {
          resolve(todos);
        }
      };

      request.onerror = (e) => reject(e.target.error);
    });
  }
}

// Использование
const app = new TodoApp();
await app.init();

await app.addTodo('Изучить IndexedDB');
await app.addTodo('Написать PWA');

const active = await app.getActiveTodos();
console.log('Активных задач:', active.length);

Когда использовать IndexedDB

IndexedDB подходит в следующих сценариях:

  • PWA и офлайн-режим: данные доступны без подключения к сети
  • Большие объёмы данных: сотни тысяч записей или файлы
  • Сложные запросы: поиск и фильтрация по нескольким полям
  • Локальный кэш: снижение нагрузки на сервер и ускорение интерфейса

Для небольших настроек и токенов достаточно localStorage. Для временного хранения в рамках одной вкладки используйте sessionStorage.

Библиотеки для упрощения работы

Нативный IndexedDB API многословен. Популярные обёртки решают эту проблему:

  • idb — тонкая Promise-обёртка от Jake Archibald, минимальный размер
  • Dexie.js — полноценный ORM с реактивностью и живыми запросами
  • localForage — единый API для IndexedDB, WebSQL и localStorage
// Пример с библиотекой idb
import { openDB } from 'idb';

const db = await openDB('myDB', 1, {
  upgrade(db) {
    const store = db.createObjectStore('users', { keyPath: 'id' });
    store.createIndex('email', 'email', { unique: true });
  }
});

await db.put('users', { id: 1, name: 'Тест', email: 'test@example.com' });
const user = await db.get('users', 1);
console.log(user);

// С Dexie.js
import Dexie from 'dexie';

const db = new Dexie('myDatabase');
db.version(1).stores({
  users: '++id, name, email'
});

await db.users.add({ name: 'Тест', email: 'test@example.com' });
const allUsers = await db.users.toArray();

Для большинства проектов использование idb или Dexie.js предпочтительнее написания нативного кода вручную: они сокращают объём шаблонного кода и упрощают поддержку.

Чтобы освоить браузерные API и научиться строить современные веб-приложения, пройдите курс по JavaScript на PurpleSchool: https://purpleschool.ru/course/javascript?utm_source=knowledgebase&utm_medium=text&utm_campaign=indexeddb-javascript

Стрелочка влевоIntersectionObserver в JavaScriptCanvas API в JavaScriptСтрелочка вправо

Постройте личный план изучения Javascript до уровня Middle — бесплатно!

Javascript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Javascript

Как работает метод trim() - JavaScriptКак работает метод toUpperCase() - JavaScriptКак работает метод toLowerCase() - JavaScriptКак работает метод substring() - JavaScriptКак работает метод startsWith() - JavaScriptКак работает метод split() - JavaScriptКак работает метод slice() - JavaScriptКак работает метод search() - JavaScriptКак работает метод replaceAll() - JavaScriptКак работает метод replace() - JavaScriptКак работает метод repeat() - JavaScriptКак работает метод padStart() - JavaScriptКак работает метод padEnd() - JavaScriptКак работает метод matchAll() - JavaScriptКак работает метод match() - JavaScriptКак работает метод localeCompare() - JavaScriptКак работает свойство length - JavaScriptКак работает метод lastIndexOf() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод fromCodePoint() - JavaScriptКак работает метод fromCharCode() - JavaScriptКак работает метод endsWith() - JavaScriptКак работает метод concat() - JavaScriptКак работает метод codePointAt() - JavaScriptКак работает метод charAt() - JavaScriptКак работает метод charCodeAt() - JavaScript
Итератор в JavaScript
try...catch в JavaScriptError в JavaScript
Событие wheel в JavaScriptСобытие touch в JavaScriptСобытие unload в JavaScriptСобытие submit в JavaScriptСобытие scroll в JavaScriptСобытие reset в JavaScriptМетод .preventDefault() в JavaScriptСобытие mouseover в JavaScriptСобытие mouseout в JavaScriptСобытие load в JavaScriptСобытие keyup в JavaScriptСобытие keydown в JavaScriptСобытие invalid в JavaScriptСобытие input в JavaScriptСобытийная модель Event в JavaScriptОбъект события Event в JavaScriptСобытие DOMContentLoaded в JavaScriptСобытие dblclick в JavaScriptСобытие click в JavaScriptСобытие change в JavaScriptBroadcast Channel API в JavaScriptJavaScript BroadcastChannel — межвкладочное взаимодействиеСобытие beforeunload в JavaScript
Error cause — цепочки ошибок в JavaScript
Методы массивов toSorted, toReversed и withКак работает метод some() - JavaScriptКак работает метод reverse() - JavaScriptКак работает метод reduce() - JavaScriptКак работает метод map() - JavaScriptКак работает метод isArray() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод from() - JavaScriptКак работает метод forEach() - JavaScriptКак работает метод flatMap() - JavaScriptКак работает метод flat() - JavaScriptКак работает метод findIndex() - JavaScriptКак работает метод find() - JavaScriptКак работает метод filter() - JavaScriptКак работает метод every() - JavaScriptМассивы в JavaScriptArray.at, findLast, findLastIndex — новые методы массивов
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы JavaScript

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий