Кэширование результатов запросов в Dexie.js опирается на особенности IndexedDB как долговременного клиентского хранилища и на модель асинхронных транзакций, позволяющую безопасно сохранять, переиспользовать и инвалидировать ранее вычисленные данные. Основная цель кэширования в данном контексте — снижение числа обращений к источнику данных (серверу или вычислительно дорогим операциям), ускорение повторных запросов и обеспечение оффлайн-доступа к уже полученной информации.
IndexedDB не предоставляет встроенного уровня HTTP-кэша или query-cache, поэтому кэширование реализуется на уровне прикладной логики. Dexie.js упрощает работу с этой моделью за счёт декларативного описания схемы и удобного API для транзакций.
Типичная модель кэша включает:
Простейшая структура таблицы:
db.cache.store('requests', {
key: 'string',
data: 'any',
upd atedAt: 'number',
ttl: 'number'
});
На практике схема описывается через versioned schema Dexie:
const db = new Dexie('app');
db.version(1).stores({
cache: 'key, upd atedAt'
});
Наиболее распространённая стратегия кэширования в Dexie — cache-aside (ленивый кэш):
Пример реализации:
async function getUsers() {
const cached = await db.cache.get('users:list');
if (cached && !isExpired(cached)) {
return cached.data;
}
const response = await fetch('/api/users');
const data = await response.json();
await db.cache.put({
key: 'users:list',
data,
updatedAt: Date.now(),
ttl: 60_000
});
return data;
}
Функция isExpired обычно реализуется через сравнение
текущего времени с updatedAt + ttl.
Ключевой аспект надёжного кэширования — стратегия инвалидирования. В Dexie.js это реализуется явно, так как автоматических механизмов нет.
Подход основан на проверке срока жизни записи:
function isExpired(entry) {
return Date.now() > entry.updatedAt + entry.ttl;
}
TTL подходит для данных с предсказуемой частотой обновления: справочники, списки категорий, публичные данные.
Используется при изменении данных в приложении:
async function updateUser(user) {
await fetch(`/api/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify(user)
});
await db.cache.delete('users:list');
}
Здесь кэш удаляется полностью, что гарантирует консистентность при следующем запросе.
При изменении структуры данных или логики запроса используется ключ с версией:
const cacheKey = `users:list:v2`;
Этот подход позволяет избежать массового очищения таблицы и постепенно мигрировать к новому формату.
Для сложных запросов кэш-ключ должен однозначно отражать параметры:
function buildCacheKey(params) {
return `users:${params.page}:${params.sort}:${params.filter}`;
}
Важно нормализовать параметры:
Без нормализации кэш теряет эффективность из-за дублирования записей.
Dexie позволяет строить запросы поверх индексов, но сам по себе результат query не кэшируется. Поэтому создаётся слой обёртки:
async function cachedQuery(key, queryFn, ttl = 60000) {
const cached = await db.cache.get(key);
if (cached && !isExpired(cached)) {
return cached.data;
}
const result = await queryFn();
await db.cache.put({
key,
data: result,
updatedAt: Date.now(),
ttl
});
return result;
}
Использование:
const users = await cachedQuery(
'users:active',
() => db.users.where('active').equals(1).toArray(),
30000
);
При работе с коллекциями часто выгоднее кэшировать не весь результат, а нормализованные сущности.
Структура:
usersqueriescacheIndexПример:
db.version(1).stores({
users: 'id',
cacheIndex: 'key'
});
Схема:
Это уменьшает дублирование данных и упрощает инвалидирование.
Вместо полного обновления данных применяется стратегия patch-update:
async function syncUsers() {
const updates = await fetch('/api/users/changes');
await db.transaction('rw', db.users, async () => {
for (const user of updates) {
await db.users.put(user);
}
});
}
Кэш запросов перестаёт быть источником истины и становится проекцией локальной базы.
Dexie.js предоставляет механизм реактивных запросов через
liveQuery, который изменяет подход к кэшированию. Вместо
ручного контроля обновлений данные автоматически пересчитываются при
изменении таблиц.
import { liveQuery } fr om 'dexie';
const activeUsers$ = liveQuery(() =>
db.users.wh ere('active').equals(1).toArray()
);
В этом случае классическое кэширование становится избыточным, но может использоваться как слой ускорения первичной загрузки.
Практическая архитектура часто включает несколько уровней:
Схема доступа:
Пример memory-слоя:
const memoryCache = new Map();
async function getData(key, loader) {
if (memoryCache.has(key)) {
return memoryCache.get(key);
}
const cached = await db.cache.get(key);
if (cached && !isExpired(cached)) {
memoryCache.se t(key, cached.data);
return cached.data;
}
const data = await loader();
memoryCache.se t(key, data);
await db.cache.put({ key, data, upd atedAt: Date.now(), ttl: 60000 });
return data;
}
При параллельных вызовах одного и того же запроса возможна гонка на запись. Решается через in-flight cache:
const inFlight = new Map();
function deduped(key, fn) {
if (inFlight.has(key)) {
return inFlight.get(key);
}
const promise = fn().finally(() => {
inFlight.delete(key);
});
inFlight.se t(key, promise);
return promise;
}
Интеграция с Dexie-кэшем предотвращает дублирующие сетевые запросы.
IndexedDB имеет ограничения по квотам браузера, поэтому кэш часто оптимизируют:
Пример агрегации:
const summary = {
total: users.length,
active: users.filter(u => u.active).length
};
Вместо хранения полного массива может храниться агрегат плюс ключи.
Регулярная очистка выполняется через batch-процедуры:
async function cleanCache() {
const now = Date.now();
await db.cache
.where('updatedAt')
.below(now - 24 * 60 * 60 * 1000)
.delete();
}
Также используется стратегия LRU на уровне приложения, если требуется ограничение размера.
Dexie не поддерживает SQL join напрямую, поэтому результат сложных выборок часто кэшируется целиком:
async function getDashboard() {
return cachedQuery('dashboard:v1', async () => {
const users = await db.users.toArray();
const orders = await db.orders.toArray();
return {
users,
orders
};
});
}
Такой подход переносит вычисления из runtime в слой кэша, снижая нагрузку на повторные вычисления.