Кеширование данных

Роль кеширования при работе с удалёнными источниками

При использовании Tom Select в режиме загрузки данных с сервера основная нагрузка ложится на функцию load(query, callback), которая вызывается при каждом вводе пользователя. Без кеширования одинаковые запросы повторяются, создавая избыточные сетевые обращения, увеличивая задержки интерфейса и нагрузку на API.

Кеширование в данном контексте решает три ключевые задачи:

  • сокращение количества HTTP-запросов;
  • ускорение повторного отображения уже запрашиваемых данных;
  • снижение зависимости UI от сетевой задержки.

Особенно критично это поведение при поиске по справочникам, API с лимитами или при работе с медленными источниками данных.


Базовая модель кеширования через обёртку load

Tom Select не навязывает встроенный слой кеширования, но позволяет легко реализовать его поверх стандартного load.

Классическая схема основана на использовании in-memory структуры:

const cache = new Map();

new TomSelect("#select", {
  valueField: "id",
  labelField: "title",
  searchField: "title",

  load: function(query, callback) {
    if (cache.has(query)) {
      callback(cache.get(query));
      return;
    }

    fetch(`/api/search?q=${encodeURIComponent(query)}`)
      .then(res => res.json())
      .then(data => {
        cache.set(query, data);
        callback(data);
      })
      .catch(() => callback());
  }
});

Здесь ключом кеша выступает строка запроса query. Это простая, но эффективная стратегия для большинства поисковых сценариев.


Нормализация ключей кеша

Одной из распространённых проблем становится неоднозначность пользовательского ввода. Запросы "Apple", "apple" и " apple " с точки зрения сервера могут быть эквивалентны, но в кеше будут различаться.

Решение заключается в нормализации ключа:

function normalizeQuery(query) {
  return query.trim().toLowerCase();
}

Использование:

const key = normalizeQuery(query);

Дополнительно могут применяться:

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

TTL-кеширование и устаревание данных

В реальных приложениях данные могут изменяться, поэтому простого кеша недостаточно. Добавляется механизм времени жизни (TTL — time to live).

const cache = new Map();
const TTL = 60 * 1000; // 1 минута

function setCache(key, data) {
  cache.set(key, {
    data,
    time: Date.now()
  });
}

function getCache(key) {
  const entry = cache.get(key);
  if (!entry) return null;

  const isExpired = Date.now() - entry.time > TTL;
  if (isExpired) {
    cache.delete(key);
    return null;
  }

  return entry.data;
}

Интеграция в Tom Select:

load: function(query, callback) {
  const key = normalizeQuery(query);
  const cached = getCache(key);

  if (cached) {
    callback(cached);
    return;
  }

  fetch(`/api/search?q=${encodeURIComponent(query)}`)
    .then(res => res.json())
    .then(data => {
      setCache(key, data);
      callback(data);
    });
}

Кеширование с учётом пагинации

При постраничной загрузке одного ключа query недостаточно. Данные зависят от page, limit и других параметров.

Расширенная стратегия ключа:

function makeCacheKey(query, page) {
  return `${normalizeQuery(query)}::page=${page}`;
}

Пример использования:

load: function(query, callback) {
  const page = this.currentPage || 1;
  const key = makeCacheKey(query, page);

  const cached = getCache(key);
  if (cached) {
    callback(cached);
    return;
  }

  fetch(`/api/search?q=${query}&page=${page}`)
    .then(r => r.json())
    .then(data => {
      setCache(key, data);
      callback(data);
    });
}

Предзагрузка и прогрев кеша

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

const preloadQueries = ["a", "b", "c"];

preloadQueries.forEach(q => {
  fetch(`/api/search?q=${q}`)
    .then(r => r.json())
    .then(data => setCache(normalizeQuery(q), data));
});

Такая стратегия особенно полезна для справочников с предсказуемыми значениями или часто используемыми фильтрами.


Ограничение размера кеша (LRU-подобное поведение)

При длительной работе интерфейса Map-кеш может расти бесконечно. Для предотвращения деградации памяти используется ограничение размера.

const cache = new Map();
const MAX_CACHE_SIZE = 100;

function setCache(key, data) {
  if (cache.has(key)) {
    cache.delete(key);
  }

  cache.set(key, {
    data,
    time: Date.now()
  });

  if (cache.size > MAX_CACHE_SIZE) {
    const oldestKey = cache.keys().next().value;
    cache.delete(oldestKey);
  }
}

Такой подход приближён к LRU-логике без полной реализации алгоритма вытеснения.


Совместимость кеша с debounce и throttle

Tom Select может ограничивать частоту вызова load через параметр loadThrottle. Однако кеширование и throttling решают разные задачи:

  • throttle уменьшает частоту запросов;
  • кеш устраняет повторные запросы.

Совместное использование:

new TomSelect("#select", {
  loadThrottle: 300,

  load: function(query, callback) {
    const key = normalizeQuery(query);
    const cached = getCache(key);

    if (cached) {
      callback(cached);
      return;
    }

    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => {
        setCache(key, data);
        callback(data);
      });
  }
});

Кеширование с учётом отмены запросов

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

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

let controller = null;

load: function(query, callback) {
  const key = normalizeQuery(query);
  const cached = getCache(key);

  if (cached) {
    callback(cached);
    return;
  }

  if (controller) {
    controller.abort();
  }

  controller = new AbortController();

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then(r => r.json())
    .then(data => {
      setCache(key, data);
      callback(data);
    })
    .catch(err => {
      if (err.name !== "AbortError") {
        callback();
      }
    });
}

Кеширование на уровне экземпляра и глобального состояния

В зависимости от архитектуры приложения кеш может быть:

Локальным для компонента

new TomSelect("#select", {
  load: function(query, callback) {
    // cache внутри замыкания
  }
});

Глобальным для всего приложения

window.tsCache = window.tsCache || new Map();

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


Сериализация кеша в localStorage

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

function saveCacheToStorage(cache) {
  localStorage.setItem("ts_cache", JSON.stringify([...cache]));
}

function loadCacheFromStorage() {
  const raw = localStorage.getItem("ts_cache");
  return raw ? new Map(JSON.parse(raw)) : new Map();
}

Ограничения такого подхода:

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

Кеширование результатов преобразования данных

Иногда кешируется не только ответ API, но и результат его обработки перед передачей в Tom Select.

function transform(data) {
  return data.map(item => ({
    id: item.id,
    title: item.name
  }));
}

Кеширование после трансформации снижает повторные вычисления:

setCache(key, transform(data));

Поведение кеша при динамических опциях

При изменении параметров селекта (например, фильтров) кеш должен учитывать контекст:

function makeKey(query, filters) {
  return `${query}::${JSON.stringify(filters)}`;
}

Иначе возможна подмена результатов при одинаковом запросе в разных состояниях UI.


Оптимизация стратегии кеширования

Эффективная стратегия кеширования в Tom Select обычно комбинирует несколько механизмов:

  • in-memory Map для быстрого доступа;
  • TTL для актуальности данных;
  • ограничение размера кеша;
  • AbortController для предотвращения гонок;
  • нормализация ключей для консистентности;
  • расширенные ключи с учётом параметров запроса.

Такой слой превращает load из простого HTTP-вызова в управляемую систему получения данных с контролем состояния и производительности.