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

При работе с автодополнением одной из наиболее распространённых проблем становится повторная загрузка одинаковых данных. Если список подсказок формируется через AJAX-запросы, каждое новое нажатие клавиши может инициировать обращение к серверу. Это увеличивает нагрузку на backend, ухудшает отзывчивость интерфейса и создаёт лишний сетевой трафик.

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

Основные преимущества:

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

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


Простейшее кэширование в объекте

Наиболее распространённый вариант — хранение результатов запросов в обычном JavaScript-объекте.

Базовая схема

const cache = {};

Ключом выступает поисковая строка, а значением — массив результатов.


Кэширование AJAX-запросов

HTML

<input id="city">

JavaScript

const input = document.querySelector("#city");

const awesomplete = new Awesomplete(input);

const cache = {};

input.addEventListener("input", async () => {

    const query = input.value.trim();

    if (query.length < 2) {
        return;
    }

    if (cache[query]) {

        console.log("Данные получены из кэша");

        awesomplete.list = cache[query];

        return;
    }

    console.log("Запрос к серверу");

    const response = await fetch(`/api/cities?q=${query}`);

    const data = await response.json();

    cache[query] = data;

    awesomplete.list = data;
});

Как работает механизм

Последовательность действий:

  1. Пользователь вводит текст.

  2. Проверяется наличие данных в кэше.

  3. Если данные существуют:

    • запрос не выполняется;
    • список берётся из памяти.
  4. Если данных нет:

    • выполняется fetch;
    • результаты сохраняются;
    • Awesomplete обновляет список.

Проблема точного совпадения ключей

Такой подход имеет ограничение.

Например:

"mos"

и

"mosc"

будут храниться как разные записи.

Это означает:

cache = {
    "mos": [...],
    "mosc": [...]
};

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


Частичное кэширование по префиксам

Более эффективный подход — использование ранее загруженных данных для последующих запросов.

Пример логики

Если уже загружен список для:

mos

то при вводе:

mosc

можно фильтровать существующий массив локально.


Реализация префиксного кэша

const cache = {};

function findCachedData(query) {

    const keys = Object.keys(cache);

    for (const key of keys) {

        if (query.startsWith(key)) {
            return cache[key];
        }
    }

    return null;
}

Использование локальной фильтрации

input.addEventListener("input", async () => {

    const query = input.value.trim();

    if (query.length < 2) {
        return;
    }

    const cached = findCachedData(query);

    if (cached) {

        console.log("Используется частичный кэш");

        const filtered = cached.filter(item =>
            item.toLowerCase().includes(query.toLowerCase())
        );

        awesomplete.list = filtered;

        return;
    }

    const response = await fetch(`/api/cities?q=${query}`);

    const data = await response.json();

    cache[query] = data;

    awesomplete.list = data;
});

Кэширование с ограничением времени жизни

Данные могут устаревать. Особенно это важно для:

  • поиска товаров;
  • динамических каталогов;
  • финансовых данных;
  • расписаний;
  • пользовательских списков.

В этом случае кэш должен автоматически инвалидироваться.


TTL-кэш

TTL (Time To Live) — время жизни записи.

Структура записи

cache[query] = {
    data: [...],
    timestamp: Date.now()
};

Проверка актуальности

const TTL = 60000;
function isExpired(entry) {

    return Date.now() - entry.timestamp > TTL;
}

Полный пример

const cache = {};

const TTL = 60000;

input.addEventListener("input", async () => {

    const query = input.value.trim();

    if (query.length < 2) {
        return;
    }

    const cached = cache[query];

    if (cached && !isExpired(cached)) {

        console.log("Данные из кэша");

        awesomplete.list = cached.data;

        return;
    }

    const response = await fetch(`/api/search?q=${query}`);

    const data = await response.json();

    cache[query] = {
        data,
        timestamp: Date.now()
    };

    awesomplete.list = data;
});

function isExpired(entry) {

    return Date.now() - entry.timestamp > TTL;
}

Удаление устаревших записей

Без очистки кэш постепенно увеличивается в памяти.

Очистка вручную

function cleanupCache() {

    for (const key in cache) {

        if (isExpired(cache[key])) {
            delete cache[key];
        }
    }
}

Периодическая очистка

setInterval(cleanupCache, 30000);

Ограничение размера кэша

При интенсивном использовании количество записей может стать слишком большим.

Ограничение по числу элементов

const MAX_CACHE_SIZE = 50;

Удаление старых записей

function limitCacheSize() {

    const keys = Object.keys(cache);

    if (keys.length <= MAX_CACHE_SIZE) {
        return;
    }

    delete cache[keys[0]];
}

Кэширование через Map

Map удобнее обычного объекта:

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

Пример с Map

const cache = new Map();

async function loadData(query) {

    if (cache.has(query)) {

        return cache.get(query);
    }

    const response = await fetch(`/api/search?q=${query}`);

    const data = await response.json();

    cache.set(query, data);

    return data;
}

Реализация LRU-кэша

LRU (Least Recently Used) удаляет наименее используемые записи.

Это особенно полезно для больших приложений.


Пример LRU-алгоритма

const MAX_CACHE_SIZE = 20;

const cache = new Map();

function saveToCache(key, value) {

    if (cache.has(key)) {
        cache.delete(key);
    }

    cache.set(key, value);

    if (cache.size > MAX_CACHE_SIZE) {

        const oldestKey = cache.keys().next().value;

        cache.delete(oldestKey);
    }
}

Получение данных с обновлением приоритета

function getFromCache(key) {

    if (!cache.has(key)) {
        return null;
    }

    const value = cache.get(key);

    cache.delete(key);

    cache.set(key, value);

    return value;
}

Кэширование в localStorage

Память JavaScript очищается после перезагрузки страницы. Для постоянного хранения используется localStorage.


Сохранение данных

localStorage.setItem(
    "cities_cache",
    JSON.stringify(data)
);

Загрузка данных

const data = JSON.parse(
    localStorage.getItem("cities_cache")
);

Интеграция с Awesomplete

const saved = localStorage.getItem("cities");

if (saved) {

    awesomplete.list = JSON.parse(saved);
}

Кэширование результатов API

async function fetchWithCache(query) {

    const key = `search_${query}`;

    const cached = localStorage.getItem(key);

    if (cached) {

        return JSON.parse(cached);
    }

    const response = await fetch(`/api/search?q=${query}`);

    const data = await response.json();

    localStorage.setItem(
        key,
        JSON.stringify(data)
    );

    return data;
}

Недостатки localStorage

Ограничение размера

Большинство браузеров ограничивают объём:

5–10 МБ

Синхронная работа

localStorage блокирует основной поток.

При больших объёмах данных возможны задержки интерфейса.


Отсутствие автоматической очистки

Необходимо самостоятельно:

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

IndexedDB для крупных кэшей

Если объём данных велик, используется IndexedDB.

Преимущества:

  • асинхронная работа;
  • хранение больших объёмов;
  • поддержка индексов;
  • высокая производительность.

Пример сохранения

const request = indexedDB.open("awesompleteDB", 1);

request.onupgradenee ded = event => {

    const db = event.target.result;

    db.createObjectStore("search");
};

Запись данных

function saveData(query, data) {

    const transaction = db.transaction(
        ["search"],
        "readwrite"
    );

    const store = transaction.objectStore("search");

    store.put(data, query);
}

Чтение данных

function loadData(query) {

    return new Promise(resolve => {

        const transaction = db.transaction(
            ["search"],
            "readonly"
        );

        const store = transaction.objectStore("search");

        const request = store.get(query);

        request.onsucc ess = () => {
            resolve(request.result);
        };
    });
}

Debounce и кэширование

Кэширование почти всегда используется совместно с debounce.

Debounce уменьшает количество запросов, а кэш предотвращает повторные запросы.


Комбинированный подход

let timeout;

input.addEventListener("input", () => {

    clearTimeout(timeout);

    timeout = setTimeout(async () => {

        const query = input.value.trim();

        if (cache[query]) {

            awesomplete.list = cache[query];

            return;
        }

        const response = await fetch(`/api/search?q=${query}`);

        const data = await response.json();

        cache[query] = data;

        awesomplete.list = data;

    }, 300);
});

Предварительное кэширование

Некоторые данные можно загрузить заранее.

Например:

  • список стран;
  • категории товаров;
  • города;
  • теги.

Пример preload

async function preload() {

    const response = await fetch("/api/countries");

    const data = await response.json();

    cache["countries"] = data;
}

Использование предзагруженных данных

awesomplete.list = cache["countries"];

Кэширование с Service Worker

Service Worker позволяет:

  • перехватывать запросы;
  • хранить ответы;
  • обеспечивать офлайн-доступ.

Пример перехвата запросов

self.addEventListener("fetch", event => {

    event.respondWith(

        caches.match(event.request)
            .then(response => {

                return response || fetch(event.request);
            })
    );
});

Когда кэширование особенно полезно

Большие словари

Например:

  • страны;
  • города;
  • товары;
  • имена пользователей.

Медленные API

При высокой сетевой задержке кэш значительно ускоряет интерфейс.


Ограниченные API

Некоторые сервисы имеют лимиты запросов.

Кэш помогает избежать превышения квот.


Мобильные устройства

Снижается расход трафика и энергопотребление.


Потенциальные проблемы

Устаревшие данные

Решение:

  • TTL;
  • ручная инвалидация;
  • периодическое обновление.

Рост потребления памяти

Решение:

  • ограничение размера;
  • LRU;
  • очистка старых записей.

Дублирование записей

Решение:

  • нормализация ключей;
  • приведение к нижнему регистру;
  • удаление лишних пробелов.

Нормализация поисковых запросов

function normalize(query) {

    return query
        .trim()
        .toLowerCase();
}

Использование нормализации

const key = normalize(input.value);

Кэширование и пользовательский опыт

Грамотно реализованный кэш создаёт ощущение мгновенной работы интерфейса.

Особенно заметны улучшения:

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

В системах автодополнения кэширование практически всегда является обязательной частью архитектуры.