Сравнение с другими обёртками: localForage, idb, PouchDB

Экосистема браузерного хранения данных содержит множество библиотек, скрывающих сложность нативного IndexedDB API. Среди наиболее популярных решений выделяются Dexie.js, localForage, idb и PouchDB. Несмотря на то что все они связаны с хранением данных на стороне клиента, их архитектура, назначение и уровень абстракции существенно отличаются.

Dexie.js ориентирован на полноценную работу с IndexedDB как с базой данных. Библиотека предоставляет декларативное описание схемы, индексы, сложные запросы, транзакции, миграции версий и богатый API для выборок.

localForage представляет собой простой асинхронный слой хранения данных по принципу key-value.

idb является минималистичной обёрткой над IndexedDB, практически не меняющей исходную концепцию API.

PouchDB реализует документно-ориентированную базу данных с возможностью синхронизации с удалёнными серверами.

Выбор между этими решениями определяется сложностью проекта, требованиями к запросам, объёмом данных и необходимостью синхронизации.


Сравнение Dexie.js и localForage

Философия библиотек

Главное различие заключается в уровне абстракции.

localForage стремится заменить LocalStorage более современным механизмом хранения. Внутри она может использовать IndexedDB, WebSQL или LocalStorage, скрывая детали реализации.

Пример записи данных в localForage:

await localforage.setItem("user", {
    id: 1,
    name: "Alex"
});

Получение:

const user = await localforage.getItem("user");

Разработчик работает исключительно с ключами и значениями.

Dexie.js, напротив, рассматривает IndexedDB как полноценную базу данных.

const db = new Dexie("AppDB");

db.version(1).stores({
    users: "++id, name, age"
});

await db.users.add({
    name: "Alex",
    age: 25
});

Здесь появляются таблицы, индексы, структуры данных и запросы.


Работа с запросами

localForage не содержит встроенных механизмов поиска по полям объектов.

Если необходимо найти пользователя по имени, придётся сначала загрузить все записи и выполнить фильтрацию вручную.

const users = await localforage.getItem("users");

const result = users.filter(
    user => user.name === "Alex"
);

При больших объёмах данных такой подход становится крайне неэффективным.

Dexie.js использует индексы IndexedDB.

const result = await db.users
    .where("name")
    .equals("Alex")
    .toArray();

Поиск выполняется непосредственно внутри базы данных.


Индексы

В localForage индексы отсутствуют как концепция.

В Dexie.js индексы являются основой производительности.

db.version(1).stores({
    users: "++id, name, age, city"
});

По этим полям можно выполнять быстрые запросы:

await db.users
    .where("city")
    .equals("Berlin")
    .toArray();

Связанные данные

localForage не предоставляет механизмов организации отношений между сущностями.

При хранении связанных объектов вся логика объединения данных реализуется вручную.

Dexie.js позволяет строить полноценные структуры:

db.version(1).stores({
    users: "++id",
    orders: "++id, userId"
});

Получение заказов пользователя:

const orders = await db.orders
    .where("userId")
    .equals(userId)
    .toArray();

Транзакции

В localForage отсутствует полноценное управление транзакциями.

Несколько операций записи могут выполняться независимо друг от друга.

Dexie.js предоставляет удобный API транзакций.

await db.transaction(
    "rw",
    db.users,
    db.orders,
    async () => {

        await db.users.add(user);

        await db.orders.add(order);
    }
);

При ошибке изменения будут отменены.


Масштабируемость

localForage подходит для:

  • настроек приложения;
  • хранения токенов;
  • кэширования данных;
  • офлайн-ресурсов;
  • небольших наборов информации.

Dexie.js лучше подходит для:

  • CRM-систем;
  • сложных SPA-приложений;
  • офлайн-баз данных;
  • редакторов документов;
  • систем управления проектами;
  • приложений с десятками тысяч записей.

Сравнение Dexie.js и idb

Архитектурный подход

Обе библиотеки работают поверх IndexedDB, однако уровень абстракции отличается радикально.

idb можно рассматривать как тонкую обёртку над нативным API.

Dexie.js является полноценным ORM-подобным слоем.


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

В idb:

import { openDB } fr om "idb";

const db = await openDB(
    "AppDB",
    1,
    {
        upgrade(db) {
            db.createObjectStore(
                "users",
                {
                    keyPath: "id",
                    autoIncrement: true
                }
            );
        }
    }
);

В Dexie.js:

const db = new Dexie("AppDB");

db.version(1).stores({
    users: "++id"
});

Код становится значительно компактнее.


Выполнение запросов

idb:

const tx = db.transaction("users");

const store = tx.objectStore("users");

const user = await store.get(1);

Dexie.js:

const user = await db.users.get(1);

Dexie.js скрывает большую часть низкоуровневых деталей.


Сложные выборки

В idb необходимо вручную работать с индексами и курсорами.

Например:

const tx = db.transaction("users");

const index = tx
    .objectStore("users")
    .index("name");

const users = await index.getAll("Alex");

В Dexie.js:

const users = await db.users
    .wh ere("name")
    .equals("Alex")
    .toArray();

API больше напоминает запросы к реляционным базам данных.


Курсоры

idb предоставляет почти прямой доступ к IndexedDB-курсорам.

let cursor = await store.openCursor();

while (cursor) {
    console.log(cursor.value);

    cursor = await cursor.continue();
}

Dexie.js позволяет использовать более высокоуровневые методы.

await db.users.each(user => {
    console.log(user);
});

Размер библиотеки

Одним из преимуществ idb является минимальный размер.

Библиотека практически не добавляет дополнительной логики поверх IndexedDB.

Dexie.js заметно крупнее, поскольку содержит:

  • систему запросов;
  • миграции;
  • транзакционный слой;
  • хуки;
  • расширения;
  • дополнительные механизмы оптимизации.

Если проекту нужна только удобная Promise-обёртка над IndexedDB, idb может оказаться предпочтительнее.


Производительность

Поскольку idb является очень тонким слоем, её накладные расходы минимальны.

Dexie.js добавляет собственную инфраструктуру, однако на практике разница обычно несущественна по сравнению со временем выполнения операций IndexedDB.

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


Сравнение Dexie.js и PouchDB

Различие в концепции хранения

Dexie.js является обёрткой над IndexedDB.

PouchDB представляет собой полноценную документную базу данных, вдохновлённую CouchDB.

Вместо таблиц используются документы.

Пример документа PouchDB:

await db.put({
    _id: "user_1",
    name: "Alex",
    age: 25
});

Каждая запись хранится как отдельный документ JSON.


Организация данных

Dexie.js:

db.version(1).stores({
    users: "++id, name",
    orders: "++id, userId"
});

Структура заранее описывается через схему.

PouchDB:

await db.put({
    _id: "user_1",
    type: "user",
    name: "Alex"
});

Схема фактически отсутствует.

Разные документы могут иметь различную структуру.


Индексы и запросы

Dexie.js использует индексы IndexedDB напрямую.

await db.users
    .where("name")
    .equals("Alex")
    .toArray();

В PouchDB для сложных запросов обычно требуется создание индексов отдельно.

await db.createIndex({
    index: {
        fields: ["name"]
    }
});

После этого выполняются запросы через Mango Query.

await db.find({
    selector: {
        name: "Alex"
    }
});

Синхронизация

Это главное преимущество PouchDB.

Локальная база может автоматически синхронизироваться с серверной базой CouchDB.

db.sync(remoteDb, {
    live: true,
    retry: true
});

Dexie.js не содержит встроенного универсального механизма синхронизации.

Для синхронизации необходимо самостоятельно реализовывать обмен данными либо использовать дополнительные решения.


Конфликты данных

PouchDB включает механизмы работы с конфликтами версий документов.

При синхронизации нескольких устройств возможны ситуации, когда один и тот же документ изменяется одновременно.

Система хранит ревизии и помогает разрешать конфликты.

Dexie.js не содержит встроенной системы контроля версий документов.


Работа в офлайн-режиме

Обе библиотеки хорошо подходят для офлайн-приложений.

Однако цели различаются.

Dexie.js ориентирован на локальную базу данных с быстрыми запросами и сложной структурой.

PouchDB ориентирован на офлайн-first архитектуру с последующей синхронизацией между устройствами и сервером.


Объём API

API Dexie.js остаётся относительно компактным и сосредоточенным вокруг IndexedDB.

PouchDB содержит значительно больше возможностей:

  • документы;
  • ревизии;
  • репликацию;
  • синхронизацию;
  • конфликты;
  • запросы Mango;
  • плагины.

Соответственно увеличивается сложность изучения и поддержки проекта.


Когда выбирать Dexie.js

Dexie.js оказывается наиболее удачным выбором в ситуациях, когда необходимы:

  • полноценные таблицы и коллекции;
  • сложные запросы по индексам;
  • высокая производительность выборок;
  • транзакции;
  • миграции схем;
  • работа с большими локальными наборами данных;
  • удобный и выразительный API поверх IndexedDB;
  • долгосрочное развитие клиентской базы данных.

По сравнению с localForage библиотека предоставляет значительно более мощную модель данных. По сравнению с idb предлагает гораздо более высокий уровень абстракции и удобства. По сравнению с PouchDB обеспечивает более естественную работу именно с IndexedDB и не навязывает документно-ориентированную модель хранения и механизмы синхронизации.

В результате Dexie.js занимает промежуточное положение между минималистичными обёртками и полноценными клиентскими СУБД, предоставляя богатый набор возможностей без существенного усложнения архитектуры приложения.