LRU Redux
GitHub概要
SamSaffron/lru_redux
An efficient optionally thread safe LRU Cache
スター286
ウォッチ4
フォーク20
作成日:2013年4月23日
言語:Ruby
ライセンス:MIT License
トピックス
なし
スター履歴
データ取得日時: 2025/10/22 08:07
キャッシュライブラリ
LRU Redux
概要
LRU Reduxは、Reduxアプリケーション向けのLRU(Least Recently Used)キャッシュ戦略を実装するライブラリコンセプトです。
詳細
LRU Reduxは、Redux状態管理パターンにLRU(Least Recently Used)キャッシュ戦略を統合する設計概念およびライブラリ群です。Reduxアプリケーションにおいて、APIレスポンス、計算結果、コンポーネント状態などのデータを効率的にキャッシュし、不要なAPIコールやレンダリングを削減します。Reselectライブラリのメモ化セレクターと組み合わせることで、派生データの計算コストを削減し、Redux storeから効率的にデータを取得できます。LRUアルゴリズムにより、メモリ使用量を制限しながら最近アクセスされたデータを優先的に保持し、古いデータを自動的に削除します。Redux middleware として実装することで、action dispatching時の自動キャッシュ機能、TTL(Time To Live)による期限切れ管理、キャッシュ無効化戦略などを提供します。React アプリケーションでの使用において、コンポーネントの再レンダリング最適化、API データの重複フェッチ防止、ユーザー体験の向上に大きく貢献します。
メリット・デメリット
メリット
- パフォーマンス向上: 不要なAPIコールと再レンダリングを削減
- Redux統合: 既存のRedux architecture への自然な統合
- メモリ効率: LRUアルゴリズムによる自動メモリ管理
- 開発者体験: 透明性の高いキャッシュ機能
- 柔軟な設定: TTL、サイズ制限、無効化戦略のカスタマイズ
- TypeScript対応: 型安全なキャッシュ実装
- デバッグ支援: Redux DevToolsでのキャッシュ状態可視化
デメリット
- 複雑性: 状態管理とキャッシュロジックの複雑化
- メモリ使用量: インメモリキャッシュによる追加メモリ消費
- デバッグ困難: キャッシュ関連のバグの発見・修正が困難
- 一貫性の課題: データの一貫性管理の複雑化
- 学習コスト: Redux とキャッシュ概念の両方の理解が必要
- オーバーエンジニアリング: 小規模アプリケーションでは過剰な場合がある
主要リンク
- redux-cache npm package
- lru-cache library
- Reselect library
- Redux Toolkit RTK Query
- React Redux 公式ドキュメント
- Redux 公式ドキュメント
書き方の例
Redux Middlewareとしての実装
import { LRUCache } from 'lru-cache'
import { createSlice, configureStore } from '@reduxjs/toolkit'
// LRUキャッシュの設定
const cache = new LRUCache({
max: 1000,
ttl: 1000 * 60 * 5 // 5分
})
// キャッシュミドルウェア
const cacheMiddleware = (store) => (next) => (action) => {
// キャッシュ対象のアクションを判定
if (action.type.endsWith('/fulfilled') && action.meta?.requestId) {
const cacheKey = `${action.type}-${JSON.stringify(action.meta.arg)}`
cache.set(cacheKey, action.payload)
}
// キャッシュからの取得を試行
if (action.type.endsWith('/pending')) {
const cacheKey = `${action.type.replace('/pending', '/fulfilled')}-${JSON.stringify(action.meta.arg)}`
const cachedData = cache.get(cacheKey)
if (cachedData) {
// キャッシュヒット時は fulfilled アクションを直接ディスパッチ
return next({
type: action.type.replace('/pending', '/fulfilled'),
payload: cachedData,
meta: { ...action.meta, cached: true }
})
}
}
return next(action)
}
// Reduxストアの設定
const store = configureStore({
reducer: {
api: apiSlice.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(cacheMiddleware)
})
Redux Sliceでのキャッシュ管理
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { LRUCache } from 'lru-cache'
// グローバルキャッシュ
const dataCache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 10 // 10分
})
// 非同期アクションでキャッシュ統合
export const fetchUserData = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
const cacheKey = `user-${userId}`
// キャッシュから確認
const cached = dataCache.get(cacheKey)
if (cached) {
return { ...cached, fromCache: true }
}
try {
const response = await fetch(`/api/users/${userId}`)
const userData = await response.json()
// キャッシュに保存
dataCache.set(cacheKey, userData)
return userData
} catch (error) {
return rejectWithValue(error.message)
}
}
)
// Reduxスライス
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
loading: false,
error: null,
cacheStats: {
hits: 0,
misses: 0
}
},
reducers: {
clearUserCache: (state, action) => {
if (action.payload) {
dataCache.delete(`user-${action.payload}`)
} else {
dataCache.clear()
}
},
updateCacheStats: (state) => {
state.cacheStats = {
size: dataCache.size,
calculatedSize: dataCache.calculatedSize
}
}
},
extraReducers: (builder) => {
builder
.addCase(fetchUserData.fulfilled, (state, action) => {
state.entities[action.meta.arg] = action.payload
state.loading = false
// キャッシュ統計の更新
if (action.payload.fromCache) {
state.cacheStats.hits++
} else {
state.cacheStats.misses++
}
})
}
})
export const { clearUserCache, updateCacheStats } = usersSlice.actions
export default usersSlice.reducer
ReselectとLRUキャッシュの組み合わせ
import { createSelector } from 'reselect'
import { LRUCache } from 'lru-cache'
// 計算結果のキャッシュ
const computationCache = new LRUCache({
max: 200,
ttl: 1000 * 60 * 30 // 30分
})
// ベースセレクター
const selectUsers = (state) => state.users.entities
const selectProducts = (state) => state.products.entities
const selectOrders = (state) => state.orders.entities
// キャッシュ付きの複雑な計算セレクター
export const selectUserOrderSummary = createSelector(
[selectUsers, selectProducts, selectOrders],
(users, products, orders) => {
const cacheKey = `summary-${JSON.stringify({
userCount: Object.keys(users).length,
productCount: Object.keys(products).length,
orderCount: Object.keys(orders).length
})}`
// キャッシュから確認
const cached = computationCache.get(cacheKey)
if (cached) {
return cached
}
// 重い計算処理
const summary = Object.values(orders).reduce((acc, order) => {
const user = users[order.userId]
const orderTotal = order.items.reduce((total, item) => {
const product = products[item.productId]
return total + (product?.price || 0) * item.quantity
}, 0)
acc.totalRevenue += orderTotal
acc.ordersByUser[user?.name || 'Unknown'] =
(acc.ordersByUser[user?.name || 'Unknown'] || 0) + 1
return acc
}, {
totalRevenue: 0,
ordersByUser: {},
computedAt: Date.now()
})
// キャッシュに保存
computationCache.set(cacheKey, summary)
return summary
}
)
Reactコンポーネントでの使用
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { fetchUserData, clearUserCache } from './userSlice'
// キャッシュ統計表示コンポーネント
const CacheStats = () => {
const cacheStats = useSelector(state => state.users.cacheStats)
const dispatch = useDispatch()
useEffect(() => {
const interval = setInterval(() => {
dispatch(updateCacheStats())
}, 5000) // 5秒ごとに更新
return () => clearInterval(interval)
}, [dispatch])
return (
<div className="cache-stats">
<h3>キャッシュ統計</h3>
<p>ヒット数: {cacheStats.hits}</p>
<p>ミス数: {cacheStats.misses}</p>
<p>キャッシュサイズ: {cacheStats.size}</p>
<button onClick={() => dispatch(clearUserCache())}>
キャッシュクリア
</button>
</div>
)
}
// ユーザーデータ表示コンポーネント
const UserProfile = ({ userId }) => {
const dispatch = useDispatch()
const user = useSelector(state => state.users.entities[userId])
const loading = useSelector(state => state.users.loading)
useEffect(() => {
if (!user) {
dispatch(fetchUserData(userId))
}
}, [dispatch, userId, user])
if (loading) return <div>読み込み中...</div>
if (!user) return <div>ユーザーが見つかりません</div>
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
{user.fromCache && (
<span className="cache-indicator">キャッシュから取得</span>
)}
</div>
)
}
RTK Queryとの統合
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { LRUCache } from 'lru-cache'
// カスタムキャッシュ実装
const customCache = new LRUCache({
max: 1000,
ttl: 1000 * 60 * 15 // 15分
})
// RTK Query API定義
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api/',
// カスタムキャッシュロジックの追加
prepareHeaders: (headers, { endpoint, arg }) => {
const cacheKey = `${endpoint}-${JSON.stringify(arg)}`
const cached = customCache.get(cacheKey)
if (cached) {
headers.set('X-From-Cache', 'true')
}
return headers
}
}),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `users/${id}`,
providesTags: ['User'],
// カスタムキャッシュ統合
transformResponse: (response, meta, arg) => {
const cacheKey = `getUser-${arg}`
customCache.set(cacheKey, response)
return response
}
}),
getPosts: builder.query({
query: () => 'posts',
providesTags: ['Post'],
// 選択的キャッシュ無効化
invalidatesTags: (result, error, arg) => {
if (error) return []
return ['Post']
}
})
})
})
export const { useGetUserQuery, useGetPostsQuery } = apiSlice
高度なキャッシュ戦略
// 複数レベルキャッシュシステム
class MultiLevelCache {
constructor() {
// L1: 高速アクセス(小容量)
this.l1Cache = new LRUCache({ max: 100, ttl: 1000 * 60 })
// L2: 通常アクセス(中容量)
this.l2Cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 10 })
// L3: 長期保存(大容量)
this.l3Cache = new LRUCache({ max: 5000, ttl: 1000 * 60 * 60 })
}
get(key) {
// L1から確認
let value = this.l1Cache.get(key)
if (value) {
return { value, level: 'L1' }
}
// L2から確認
value = this.l2Cache.get(key)
if (value) {
// L1に昇格
this.l1Cache.set(key, value)
return { value, level: 'L2' }
}
// L3から確認
value = this.l3Cache.get(key)
if (value) {
// L2に昇格
this.l2Cache.set(key, value)
return { value, level: 'L3' }
}
return null
}
set(key, value, priority = 'normal') {
switch (priority) {
case 'high':
this.l1Cache.set(key, value)
this.l2Cache.set(key, value)
break
case 'normal':
this.l2Cache.set(key, value)
break
case 'low':
this.l3Cache.set(key, value)
break
}
}
getStats() {
return {
l1: { size: this.l1Cache.size },
l2: { size: this.l2Cache.size },
l3: { size: this.l3Cache.size }
}
}
}
// Redux middleware で使用
const multiLevelCache = new MultiLevelCache()
const advancedCacheMiddleware = (store) => (next) => (action) => {
// 優先度に基づいたキャッシュ戦略
if (action.type.includes('user/fetch')) {
const cached = multiLevelCache.get(action.meta.arg.userId)
if (cached) {
console.log(`Cache hit from ${cached.level}`)
// キャッシュからのデータでアクションを変更
}
}
const result = next(action)
// レスポンスの優先度判定とキャッシュ
if (action.type.endsWith('/fulfilled')) {
const priority = action.meta.critical ? 'high' : 'normal'
multiLevelCache.set(action.meta.arg, action.payload, priority)
}
return result
}