Learn JavaScript

Мини-кеширование в localStorage: идея и пример

Иногда данные меняются редко, а запросы делать каждый раз не хочется. Простейший вариант - положить результат в localStorage и некоторое время использовать его как кеш.

Идея:

  • при первом запросе: загрузить данные по сети и сохранить в localStorage
  • при повторном: сначала попробовать взять из localStorage
  • при устаревании (TTL): снова сходить в сеть и обновить кеш

1) Как выглядит запись в кеше

Удобно хранить не только данные, но и время сохранения.

const cacheValue = {
  savedAt: Date.now(),
  data: { items: [1, 2, 3] },
};

Так можно понять, свежий кеш или нет.

2) Мини-функции: get/set для кеша

function cacheSet(key, data) {
  const value = {
    savedAt: Date.now(),
    data,
  };

  localStorage.setItem(key, JSON.stringify(value));
}

function cacheGet(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;

  const parsed = JSON.parse(raw);

  if (!parsed || typeof parsed !== "object") return null;
  if (!("savedAt" in parsed) || !("data" in parsed)) return null;

  return parsed;
}

3) TTL: считать кеш “свежим” только некоторое время

TTL - время жизни кеша в миллисекундах. Например, 10 минут:

const TEN_MINUTES = 10 * 60 * 1000;

Проверка свежести:

function isFresh(savedAt, ttl) {
  return Date.now() - savedAt < ttl;
}

4) Полный пример: cache-first с обновлением по сети

async function loadJSON(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  return response.json();
}

function cacheSet(key, data) {
  const value = { savedAt: Date.now(), data };
  localStorage.setItem(key, JSON.stringify(value));
}

function cacheGet(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;

  const parsed = JSON.parse(raw);
  if (!parsed || typeof parsed !== "object") return null;
  if (typeof parsed.savedAt !== "number") return null;

  return parsed;
}

function isFresh(savedAt, ttl) {
  return Date.now() - savedAt < ttl;
}

async function loadWithCache(url, options) {
  const key = options.key;
  const ttl = options.ttl;

  const cached = cacheGet(key);

  if (cached && isFresh(cached.savedAt, ttl)) {
    return cached.data;
  }

  const data = await loadJSON(url);
  cacheSet(key, data);
  return data;
}

async function run() {
  try {
    const data = await loadWithCache("/api/data", {
      key: "cache:api:data",
      ttl: 5 * 60 * 1000,
    });

    console.log(data);
  } catch (e) {
    console.log(e.message);
  }
}

run();

5) Если localStorage сломан или переполнен

localStorage может быть недоступен (режимы приватности, ограничения) или переполнен. Тогда setItem может выбросить ошибку.

Простой безопасный вариант - оборачивать запись в try/catch:

function cacheSetSafe(key, data) {
  try {
    const value = { savedAt: Date.now(), data };
    localStorage.setItem(key, JSON.stringify(value));
  } catch (e) {
    return;
  }
}

6) Поведение “если сеть упала”: вернуть старый кеш

Иногда лучше показать устаревшие данные, чем ничего. Тогда логика меняется: при ошибке сети - вернуть кеш, даже если он старый.

async function loadWithCacheFallback(url, options) {
  const key = options.key;
  const ttl = options.ttl;

  const cached = cacheGet(key);

  if (cached && isFresh(cached.savedAt, ttl)) {
    return cached.data;
  }

  try {
    const data = await loadJSON(url);
    cacheSet(key, data);
    return data;
  } catch (e) {
    if (cached) {
      return cached.data;
    }
    throw e;
  }
}

Что стоит запомнить

  • Кеш в localStorage удобен для редко меняющихся данных.
  • Хранить лучше вместе с savedAt, чтобы проверять TTL.
  • Мини-алгоритм: кеш свежий - вернуть, иначе - сеть - сохранить - вернуть.
  • На ошибках сети можно возвращать старый кеш как fallback.
  • JSON.parse и localStorage.setItem могут выбрасывать ошибки - это стоит учитывать.