Профилирование операций с IndexedDB
Производительность IndexedDB редко определяется только скоростью самой базы данных. Основное влияние оказывают сериализация данных, частота транзакций, конкуренция за события основного потока и особенности драйвера браузера. При использовании Dexie.js эти факторы остаются критичными, несмотря на упрощённый API.
Базовый уровень измерений строится вокруг понимания границ операции: транзакция, запрос, батч, цепочка промисов. Ошибка на этом уровне приводит к неверной интерпретации узких мест — вместо базы данных оптимизируется JavaScript-код или наоборот.
Для первичной оценки времени выполнения операций применяется User Timing API:
```javascript
performance.mark('db-start');
await db.users.bulkAdd(users);
performance.mark('db-end');
performance.measure('bulkAdd users', 'db-start', 'db-end');
console.log(performance.getEntriesByName('bulkAdd users'));
```
Dexie.js не скрывает асинхронность IndexedDB, поэтому такие измерения отражают реальную стоимость транзакции вместе с микрозадержками планировщика событий.
---
### Транзакционные границы как источник задержек
IndexedDB группирует операции в транзакции, и Dexie строго следует этой модели. Каждое обращение к таблице может создавать новую транзакцию, если разработчик явно не контролирует область выполнения.
Наиболее дорогим аспектом становится не сама операция чтения или записи, а создание и завершение транзакции:
* инициализация транзакции в движке браузера;
* захват блокировок object store;
* commit/abort фазы;
* синхронизация с дисковым слоем.
Dexie позволяет явно управлять границами:
```javascript
await db.transaction('rw', db.users, db.orders, async () => {
await db.users.add({ id: 1, name: 'A' });
await db.orders.add({ id: 10, userId: 1 });
});
```
При профилировании важно сравнивать сценарии «много мелких транзакций» и «одна крупная транзакция». Разница часто достигает порядков величины из-за стоимости commit-фазы.
---
### Влияние индексов и структуры схемы
Производительность IndexedDB напрямую зависит от структуры индексов. Dexie.js предоставляет декларативное описание схемы, но физическое поведение остаётся зависимым от браузера.
Пример схемы:
```javascript
const db = new Dexie('AppDB');
db.version(1).stores({
users: '++id, email, age, country',
orders: '++id, userId, createdAt'
});
```
Каждый дополнительный индекс увеличивает стоимость:
* вставки (дублирование ключей в B-tree структурах);
* обновления (перезапись нескольких веток дерева);
* удаления (поиск и очистка всех индексных записей).
Профилирование должно включать сравнение:
* запись без индексов;
* запись с минимальным набором индексов;
* запись с составными индексами.
Особенно критичны compound-индексы:
```javascript
db.version(2).stores({
orders: '++id, [userId+createdAt]'
});
```
Они ускоряют выборки, но увеличивают стоимость вставки и могут становиться узким местом при bulk-операциях.
---
### Bulk-операции и амортизация затрат
Dexie.js предоставляет оптимизированные методы массовых операций: `bulkAdd`, `bulkPut`, `bulkDelete`. Их ключевая особенность — амортизация накладных расходов транзакции.
При профилировании необходимо учитывать разницу между:
```javascript
for (const item of items) {
await db.users.add(item);
}
```
и:
```javascript
await db.users.bulkAdd(items);
```
Во втором случае:
* создаётся одна транзакция;
* минимизируется количество event-loop переключений;
* уменьшается overhead сериализации промисов.
Метрики часто показывают нелинейное ускорение: при 1000 элементов разница может быть 10–50 раз.
---
### Накладные расходы сериализации и клонирования объектов
IndexedDB использует structured clone algorithm. Dexie не заменяет этот механизм, поэтому каждая запись проходит через сериализацию.
Наиболее дорогие случаи:
* вложенные объекты с глубокими структурами;
* большие массивы;
* Blob/File объекты;
* Map/Set структуры.
Профилирование должно разделять:
* время подготовки данных в JS;
* время передачи в IndexedDB;
* время записи в хранилище.
Пример разделения:
```javascript
performance.mark('prepare-start');
const prepared = items.map(transform);
performance.mark('prepare-end');
performance.mark('write-start');
await db.table.bulkPut(prepared);
performance.mark('write-end');
```
Разделение позволяет обнаружить случаи, когда «медленная база» на самом деле является «медленной трансформацией данных».
---
### Конкуренция транзакций и блокировки
IndexedDB использует модель блокировок object store. Dexie добавляет удобство, но не меняет фундаментальную природу конкурентного доступа.
Типичные проблемы:
* долгие readwrite-транзакции блокируют другие записи;
* параллельные операции создают очередь ожидания;
* чтения могут блокироваться записью при пересечении stores.
Профилирование включает измерение времени ожидания начала транзакции, а не только её выполнения.
Практический индикатор — задержка между вызовом и фактическим стартом операции:
```javascript
const t0 = performance.now();
db.transaction('rw', db.users, async () => {
const t1 = performance.now();
console.log('transaction started after', t1 - t0);
});
```
Большие задержки часто указывают на перегруженные транзакционные очереди.
---
### Dexie hooks как инструмент наблюдения
Dexie.js предоставляет систему hooks, позволяющую измерять операции на уровне CRUD:
```javascript
db.users.hook('creating', (primKey, obj, trans) => {
obj._t0 = performance.now();
});
db.users.hook('reading', (obj) => {
obj._t1 = performance.now();
return obj;
});
```
Хотя hooks не предназначены исключительно для профилирования, они позволяют:
* измерять задержки между чтением и записью;
* фиксировать время прохождения через транзакцию;
* отслеживать горячие точки модели данных.
Особенно полезны hooks `creating`, `updating`, `deleting` для анализа write-heavy нагрузок.
---
### Использование Chrome DevTools и IndexedDB Profiler
Встроенные инструменты браузера часто дают более точную картину, чем ручные метрики.
В Chrome DevTools:
* Performance panel показывает блокировки main thread;
* Application → IndexedDB отображает структуру и размер;
* Performance Monitor фиксирует long tasks;
* Memory panel позволяет выявить утечки объектов, связанных с Dexie кешированием.
Особое внимание уделяется long tasks (>50ms), которые часто возникают при:
* массовых commit транзакций;
* обработке больших result set;
* синхронной обработке после получения данных.
---
### Асинхронные цепочки и скрытые задержки Dexie Promise pipeline
Dexie использует собственную обёртку над Promise для оптимизации транзакционного контекста. Это создаёт эффект «скрытых очередей», когда операции группируются внутри микротасков.
Проблема проявляется при цепочках:
```javascript
db.users
.where('age')
.above(18)
.toArray()
.then(process)
.then(save);
```
Каждый then может добавлять микрозадержку. При профилировании важно учитывать:
* количество переходов между микротасками;
* повторное открытие транзакции;
* неявные await внутри Dexie pipeline.
Иногда более эффективной оказывается явная агрегация:
```javascript
const users = await db.users.where('age').above(18).toArray();
const processed = process(users);
await save(processed);
```
---
### Оптимизация чтения больших наборов данных
Чтение больших объёмов данных часто становится узким местом не из-за IndexedDB, а из-за:
* аллокации массивов;
* GC pressure;
* сериализации результатов в Dexie toArray().
Альтернативный подход — использование курсоров:
```javascript
await db.users.each(user => {
handle(user);
});
```
Курсоры уменьшают пиковую нагрузку на память, но увеличивают общее время выполнения. Профилирование должно учитывать оба параметра: latency и memory footprint.
---
### Метрики, которые имеют практическое значение
При анализе производительности IndexedDB через Dexie.js ключевыми являются:
* время открытия транзакции;
* время commit;
* throughput операций в bulk;
* latency первого результата cursor/iterator;
* GC паузы после массовых операций;
* время structured clone.
Абстрактные метрики вроде «скорости базы» не дают полезной информации без разбиения на эти компоненты.
---
### Типичные ошибки интерпретации профилей
Неверная интерпретация часто приводит к ложной оптимизации:
* оптимизация JS-кода вместо уменьшения числа транзакций;
* добавление индексов без анализа write-cost;
* замена bulk операций на поэлементные await вызовы;
* игнорирование GC пауз;
* измерение только чистого выполнения без учёта очередей транзакций.
Корректный профиль всегда включает полный цикл: подготовка данных, транзакция, запись/чтение, завершение и постобработку.