Пагинация больших наборов данных в Dexie.js требует учета особенностей IndexedDB, структуры индексов и характера запросов, поскольку традиционные подходы из серверной разработки (limit/offset в SQL) здесь не всегда дают предсказуемую производительность. При работе с локальной базой ключевую роль играет не только способ разбиения данных на страницы, но и стратегия их извлечения, особенно при росте таблиц до десятков и сотен тысяч записей.
Dexie.js является тонким и удобным слоем над IndexedDB, поэтому ограничения платформы напрямую влияют на поведение пагинации:
OFFSET-механизм, как в
SQL;Классическая пагинация вида LIMIT + OFFSET в Dexie
реализуется через offset() и limit(), но
offset() фактически выполняет пропуск элементов на стороне
курсора, что при больших значениях приводит к линейной деградации
производительности.
Самый простой вариант реализуется средствами Dexie:
db.users
.orderBy('id')
.offset(page * pageSize)
.limit(pageSize)
.toArray();
Такой подход удобен для небольших объемов данных, но имеет фундаментальный недостаток: при переходе на глубокие страницы (например, page = 500) происходит последовательное прохождение сотен тысяч записей.
Более эффективный подход основан на использовании курсора и ключей последней записи предыдущей страницы. Это исключает необходимость пропуска элементов.
Каждая страница получает «якорь» — последний ключ предыдущего набора данных. Следующий запрос начинается строго после него.
db.users
.where('id')
.above(lastId)
.limit(pageSize)
.toArray();
или через startAt:
db.users
.orderBy('id')
.startAfter(lastId)
.limit(pageSize)
.toArray();
lastId или составной курсор.При сложных сортировках (например, дата + идентификатор) применяется составной индекс.
db.posts.orderBy('[createdAt+id]')
db.posts
.where('[createdAt+id]')
.above([lastDate, lastId])
.limit(pageSize)
.toArray();
createdAt;IndexedDB поддерживает диапазоны ключей через
IDBKeyRange, которые Dexie абстрагирует:
db.users
.where('id')
.between(startId, endId, true, false)
.toArray();
Этот подход полезен при:
При необходимости отображения последних записей сначала используется обратный порядок:
db.users
.orderBy('id')
.reverse()
.offset(page * pageSize)
.limit(pageSize)
.toArray();
Однако более эффективный вариант — reverse cursor:
db.users
.orderBy('id')
.below(lastId)
.reverse()
.limit(pageSize)
.toArray();
Этот метод часто применяется в чатах и логах, где новые данные важнее старых.
Бесконечная прокрутка является наиболее естественным сценарием для Dexie.js, поскольку совпадает с моделью курсора.
let lastId = null;
async function loadNextPage() {
const query = lastId
? db.users.where('id').above(lastId)
: db.users.orderBy('id');
const items = await query.limit(50).toArray();
if (items.length > 0) {
lastId = items[items.length - 1].id;
}
return items;
}
При наличии фильтров эффективность пагинации зависит от того, совпадает ли фильтр с индексом.
db.orders
.where('status')
.equals('pending')
.offset(0)
.limit(20)
Индекс по status позволяет быстро получить
подмножество.
db.orders
.filter(order => order.total > 100)
Здесь IndexedDB вынуждено просматривать все записи, что делает пагинацию неэффективной.
Оптимальная стратегия сочетает индексированный фильтр и курсор:
db.orders
.where('[status+id]')
.between(['pending', Dexie.minKey], ['pending', lastId])
.limit(20)
.toArray();
Это обеспечивает:
При сортировке по полям, которые могут изменяться (например, рейтинг, количество лайков), возникает проблема «прыгающих страниц».
Изменение значения индекса приводит к перераспределению записи в B-tree структуре IndexedDB.
Использование фиксированного вторичного ключа:
db.posts
.orderBy('[score+id]')
.reverse()
или периодическая стабилизация через серверный или локальный snapshot.
В Dexie.js данные могут изменяться между запросами страниц, что приводит к:
1. Снимок (snapshot keyset) Фиксация верхней границы выборки:
const snapshotTime = Date.now();
db.logs
.where('timestamp')
.below(snapshotTime)
2. Заморозка курсора
Использование lastKey как неизменяемого якоря:
.where('id').above(lastId)
3. Версионная пагинация
Добавление поля версии:
.where('[version+id]')
При работе с десятками тысяч записей эффективность пагинации зависит от нескольких факторов:
filter() на больших таблицах.db.users
.orderBy('id')
.limit(50)
.toArray(user => ({ id: user.id, name: user.name }))
Сокращение полей уменьшает нагрузку памяти.
each() вместо toArray()db.users
.orderBy('id')
.limit(50)
.each(user => {
// потоковая обработка
});
При частом обновлении данных (например, чаты, ленты активности) применяются гибридные стратегии:
liveQuery;Dexie.liveQuery(() =>
db.messages.orderBy('id').reverse().limit(30).toArray()
);
Такой подход разделяет исторические данные и поток новых событий.
В средах с ограниченными ресурсами (мобильные устройства, embedded WebView):
concat.Оптимальный паттерн — обработка чанков:
db.largeTable
.orderBy('id')
.eachChunk(100, chunk => {
// обработка без накопления всех данных
});
offset/limit — только для малых таблиц и
админ-интерфейсов;Совокупность этих стратегий формирует устойчивую модель работы с большими объемами данных в Dexie.js, учитывающую особенности IndexedDB и минимизирующую деградацию производительности при росте базы.