Уменьшение размера хранимых объектов

Размер объектов в хранилище Dexie.js напрямую определяется тем, какие данные попадают в IndexedDB и в каком виде они сериализуются. JavaScript-объекты сохраняются через structured clone algorithm, поэтому избыточные поля, вложенные структуры и дублирование мгновенно увеличивают объём базы.

Ключевая стратегия уменьшения размера заключается в переходе от хранения «удобных объектов приложения» к хранению «оптимизированных записей хранилища».

Удаление избыточных полей

Во многих моделях данных присутствуют поля, которые нужны только на уровне UI:

  • вычисленные строки (formattedDate, fullName)
  • временные состояния (isSelected, isExpanded)
  • метаинформация отображения
  • дублирование уже существующих данных

Такие поля не должны попадать в IndexedDB.

function prepareUserForStorage(user) {
  return {
    id: user.id,
    name: user.name,
    avatarId: user.avatarId,
    createdAt: user.createdAt
  };
}

Минимизация структуры снижает не только размер, но и стоимость индексации.

Нормализация вместо вложенности

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

Пример неэффективного хранения:

{
  id: 1,
  name: "Order",
  items: [
    { productId: 10, title: "Keyboard", price: 100 },
    { productId: 11, title: "Mouse", price: 50 }
  ]
}

Каждый заказ дублирует данные товаров, что приводит к экспоненциальному росту объёма.

Нормализованный вариант:

// Orders
{
  id: 1,
  itemIds: [10, 11]
}

// Products
{
  id: 10,
  title: "Keyboard",
  price: 100
}

Dexie.js хорошо работает с такой схемой через несколько таблиц и последующую агрегацию через join на уровне приложения.

const db = new Dexie("shop");
db.version(1).stores({
  orders: "id, date",
  products: "id, title"
});

Оптимизация схемы Dexie.js

Размер данных зависит не только от объектов, но и от структуры индексов. Каждый индекс в IndexedDB хранит дополнительную копию значений.

Избыточные индексы

Каждое поле, добавленное в stores(), увеличивает расход памяти:

db.version(1).stores({
  users: "id, email, phone, city, country, createdAt"
});

Если поле не используется для поиска, его индексация неоправданна.

Оптимизированный вариант:

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

Остальные поля остаются в объекте, но не индексируются.

Композитные индексы

Чрезмерное количество отдельных индексов иногда заменяется одним составным:

db.version(1).stores({
  logs: "id, [userId+createdAt]"
});

Это уменьшает количество индексных структур при сохранении функциональности фильтрации.


Уменьшение сериализуемого веса данных

IndexedDB хранит данные в сериализованном виде, поэтому каждый байт структуры имеет значение.

Использование коротких ключей

Длинные имена свойств увеличивают размер каждой записи:

{
  userIdentifier: 123,
  creationTimestamp: 1710000000
}

Оптимизировано:

{
  uid: 123,
  ts: 1710000000
}

На больших объёмах данных разница становится существенной.


Сжатие данных перед сохранением

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

JSON + gzip/deflate (через библиотеки)

Подход заключается в сериализации объекта в строку и последующем сжатии.

import { gzip, ungzip } fr om "pako";

function compress(data) {
  return gzip(JSON.stringify(data));
}

function decompress(binary) {
  return JSON.parse(ungzip(binary, { to: "string" }));
}

Хранение:

await db.blobs.put({
  id: 1,
  data: compress(largeObject)
});

Такой подход особенно эффективен для:

  • логов
  • кэшей API
  • больших конфигураций
  • документов с повторяющимися строками

Base64 как антипаттерн

Перевод бинарных данных в Base64 увеличивает размер примерно на 33%, поэтому используется только при необходимости совместимости.


Работа с бинарными данными вместо JSON

JSON не всегда является оптимальным форматом хранения.

ArrayBuffer и Blob

Для изображений, файлов и медиа лучше хранить бинарные данные напрямую:

await db.files.put({
  id: "img1",
  data: arrayBuffer
});

Dexie.js и IndexedDB эффективно работают с Blob и ArrayBuffer, избегая лишней сериализации.


Разбиение больших объектов на части

Очень крупные структуры целесообразно разделять на чанки.

Пример: документ с версиями

Вместо хранения всех версий в одном объекте:

{
  id: 1,
  versions: [ ...очень большой массив... ]
}

используется разбиение:

{
  docId: 1,
  versionId: 1,
  data: {...}
}

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

db.versions.wh ere("docId").equals(1).toArray();

Оптимизация строковых данных

Строки часто занимают основную часть объёма базы.

Интернирование повторяющихся значений

Если данные содержат повторяющиеся строки (статусы, категории, роли), их выгодно выносить в справочники:

// вместо:
{
  id: 1,
  status: "processing"
}

// лучше:
{
  id: 1,
  statusId: 2
}

Справочник:

{
  id: 2,
  name: "processing"
}

Контроль вложенности объектов

Каждый уровень вложенности увеличивает стоимость хранения и чтения.

Плоская структура предпочтительнее

{
  id: 1,
  user_city: "Almaty",
  user_country: "KZ"
}

вместо:

{
  id: 1,
  user: {
    location: {
      city: "Almaty",
      country: "KZ"
    }
  }
}

Плоская модель также упрощает индексацию в Dexie.


Управление датами и числами

Timestamp вместо Date

Объекты Date сериализуются с дополнительными накладными расходами.

{
  createdAt: Date.now()
}

вместо:

{
  createdAt: new Date()
}

Числовое представление компактнее и быстрее индексируется.


Снижение дублирования при кэшировании

Кэш часто становится основной причиной разрастания IndexedDB.

Общие данные вместо копий

Плохой вариант:

{
  page1: { user: {...}, posts: [...] },
  page2: { user: {...}, posts: [...] }
}

Лучше:

{
  userId: 1,
  postIds: [10, 11, 12]
}

И отдельные таблицы для сущностей.


Использование lazy-loading структуры

Хранение всех данных сразу увеличивает объём, даже если часть данных никогда не используется.

Отложенная детализация

{
  id: 1,
  title: "Article",
  contentRef: "chunk_1"
}

Тело статьи загружается только при необходимости.


Удаление устаревших и временных данных

Размер базы часто растёт из-за отсутствия стратегии очистки.

TTL-подобный подход

const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;

await db.logs.where("timestamp").below(cutoff).delete();

Регулярная очистка снижает накопление «мусорных» данных, которые не участвуют в бизнес-логике, но занимают место и индексы.


Баланс между размером и производительностью

Сильная минимизация структуры не всегда приводит к оптимальному результату. Иногда нормализация увеличивает количество чтений, а сжатие — нагрузку на CPU.

Практический баланс достигается через:

  • минимизацию индексов
  • устранение дублирования
  • нормализацию только критичных сущностей
  • компрессию только тяжёлых полей
  • хранение бинарных данных без преобразований
  • разделение «горячих» и «холодных» данных по таблицам

Такой подход позволяет удерживать размер IndexedDB под контролем без деградации скорости операций Dexie.js.