При работе с автодополнением одной из наиболее распространённых проблем становится повторная загрузка одинаковых данных. Если список подсказок формируется через AJAX-запросы, каждое новое нажатие клавиши может инициировать обращение к серверу. Это увеличивает нагрузку на backend, ухудшает отзывчивость интерфейса и создаёт лишний сетевой трафик.
Кэширование позволяет сохранять ранее полученные результаты и повторно использовать их без дополнительных запросов.
Основные преимущества:
Awesomplete не содержит встроенного механизма кэширования, однако библиотека легко интегрируется с любыми пользовательскими системами хранения данных.
Наиболее распространённый вариант — хранение результатов запросов в обычном JavaScript-объекте.
const cache = {};
Ключом выступает поисковая строка, а значением — массив результатов.
<input id="city">
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;
});
Последовательность действий:
Пользователь вводит текст.
Проверяется наличие данных в кэше.
Если данные существуют:
Если данных нет:
Такой подход имеет ограничение.
Например:
"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 (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 удобнее обычного объекта:
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 (Least Recently Used) удаляет наименее используемые записи.
Это особенно полезно для больших приложений.
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;
}
Память JavaScript очищается после перезагрузки страницы. Для
постоянного хранения используется localStorage.
localStorage.setItem(
"cities_cache",
JSON.stringify(data)
);
const data = JSON.parse(
localStorage.getItem("cities_cache")
);
const saved = localStorage.getItem("cities");
if (saved) {
awesomplete.list = JSON.parse(saved);
}
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;
}
Большинство браузеров ограничивают объём:
5–10 МБ
localStorage блокирует основной поток.
При больших объёмах данных возможны задержки интерфейса.
Необходимо самостоятельно:
Если объём данных велик, используется 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 уменьшает количество запросов, а кэш предотвращает повторные запросы.
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);
});
Некоторые данные можно загрузить заранее.
Например:
async function preload() {
const response = await fetch("/api/countries");
const data = await response.json();
cache["countries"] = data;
}
awesomplete.list = cache["countries"];
Service Worker позволяет:
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
);
});
Например:
При высокой сетевой задержке кэш значительно ускоряет интерфейс.
Некоторые сервисы имеют лимиты запросов.
Кэш помогает избежать превышения квот.
Снижается расход трафика и энергопотребление.
Решение:
Решение:
Решение:
function normalize(query) {
return query
.trim()
.toLowerCase();
}
const key = normalize(input.value);
Грамотно реализованный кэш создаёт ощущение мгновенной работы интерфейса.
Особенно заметны улучшения:
В системах автодополнения кэширование практически всегда является обязательной частью архитектуры.