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

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

Кэш может применяться к:

  • HTTP-ответам (fetch, axios)
  • результатам вычислений
  • данным, полученным из DOM
  • состоянию, зависящему от параметров

Локальный кэш в пределах контроллера

Самый простой и часто достаточный вариант — хранение данных в свойствах экземпляра контроллера.

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  cache = null

  async loadData() {
    if (this.cache) {
      return this.cache
    }

    const response = await fetch("/api/items")
    const data = await response.json()

    this.cache = data
    return data
  }
}

Особенности:

  • кэш живёт столько же, сколько и контроллер
  • данные автоматически очищаются при удалении элемента из DOM
  • подходит для данных, не требующих повторной валидации

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


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

Если результат зависит от аргументов, используется объект-хранилище или Map.

cache = new Map()

async loadUser(id) {
  if (this.cache.has(id)) {
    return this.cache.get(id)
  }

  const response = await fetch(`/api/users/${id}`)
  const user = await response.json()

  this.cache.set(id, user)
  return user
}

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

  • отсутствие конфликтов ключей
  • удобная очистка
  • корректная работа с любыми типами ключей

Кэширование на уровне модуля

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

const responseCache = new Map()

export async function cachedFetch(url) {
  if (responseCache.has(url)) {
    return responseCache.get(url)
  }

  const response = await fetch(url)
  const data = await response.json()

  responseCache.set(url, data)
  return data
}
import { Controller } from "@hotwired/stimulus"
import { cachedFetch } from "../lib/cached_fetch"

export default class extends Controller {
  async connect() {
    this.data = await cachedFetch("/api/settings")
  }
}

Применение:

  • несколько Stimulus-контроллеров используют одни и те же данные
  • необходимость избежать дублирующих запросов при инициализации

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

Для кэша, сохраняемого между перезагрузками страницы, применяются Web Storage API.

async loadConfig() {
  const cached = sessionStorage.getItem("config")

  if (cached) {
    return JSON.parse(cached)
  }

  const response = await fetch("/api/config")
  const config = await response.json()

  sessionStorage.setItem("config", JSON.stringify(config))
  return config
}

Отличия хранилищ:

  • sessionStorage — очищается при закрытии вкладки
  • localStorage — сохраняется бессрочно

Такой кэш подходит для:

  • справочных данных
  • настроек интерфейса
  • редко меняющихся ресурсов

Контроль актуальности данных

Кэш без стратегии обновления быстро теряет ценность. Распространённые подходы:

Временная инвалидизация

const CACHE_TTL = 60_000 // 1 минута

cache = {
  data: null,
  timestamp: 0
}

async load() {
  const now = Date.now()

  if (this.cache.data && now - this.cache.timestamp < CACHE_TTL) {
    return this.cache.data
  }

  const response = await fetch("/api/data")
  const data = await response.json()

  this.cache = { data, timestamp: now }
  return data
}

Принудительное обновление

async refresh() {
  this.cache = null
  return this.load()
}

Применяется при:

  • действиях пользователя, изменяющих данные
  • получении внешнего сигнала (WebSocket, SSE)

Кэширование вычисляемых значений

Stimulus-контроллеры часто выполняют вычисления на основе DOM или входных данных. Результаты можно мемоизировать.

get total() {
  if (this._total !== undefined) {
    return this._total
  }

  this._total = this.items.reduce((sum, i) => sum + i.price, 0)
  return this._total
}

При изменении исходных данных кэш сбрасывается вручную.


Интеграция с Turbo и Hotwire

При использовании Turbo Frames и Turbo Drive важно учитывать повторное подключение контроллеров. Локальный кэш контроллера не переживает навигацию, тогда как модульный или sessionStorage — переживает.

Типовая схема:

  • краткоживущий кэш — в контроллере
  • экранный кэш — в модуле
  • пользовательский кэш — в sessionStorage

Очистка и управление памятью

При агрессивном кэшировании необходимо явно освобождать ресурсы:

disconnect() {
  this.cache?.clear?.()
  this.cache = null
}

Для Map и больших структур это предотвращает накопление данных при динамическом DOM.


Ошибки и подводные камни

  • кэширование ошибок HTTP-ответов без обработки
  • отсутствие стратегии обновления
  • использование localStorage для чувствительных данных
  • зависимость кэша от устаревших параметров
  • несинхронизированное состояние между вкладками

Рекомендованные шаблоны

Минимальный кэш внутри контроллера — для UI-оптимизации Модульный кэш — для общих данных Web Storage — для сохранения состояния TTL + ручная очистка — для баланса актуальности и производительности

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