lru-cache

Node.jsキャッシュLRUアルゴリズム高性能JavaScriptキャッシュTypeScript

GitHub概要

isaacs/node-lru-cache

A fast cache that automatically deletes the least recently used items

スター5,714
ウォッチ51
フォーク364
作成日:2010年5月21日
言語:TypeScript
ライセンス:ISC License

トピックス

cachecachinglrulru-cachelrucache

スター履歴

isaacs/node-lru-cache Star History
データ取得日時: 2025/10/22 09:55

ライブラリ

lru-cache

概要

lru-cache は Node.js向け高性能LRU(Least Recently Used)キャッシュライブラリです。最近使用された順序に基づく自動的な容量管理により、メモリ効率的なキャッシュ運用を実現。 TypeScript で書き直されたバージョン7以降では大幅な性能向上を実現し、2025年現在JavaScript/Node.jsエコシステムで最もパフォーマンスの優れたLRUキャッシュ実装として広く利用されています。get操作に最適化された設計により、繰り返しアクセスが多いアプリケーションで特に優秀な性能を発揮します。

詳細

lru-cache v10系列は2025年現在も活発に開発されており、Node.js 16.14.0以降での最適化と完全なTypeScriptサポートを提供。バージョン7で内部アルゴリズムとデータ構造を根本的に書き直し、大幅なパフォーマンス向上を実現。Named exportのみ採用し、CJS/MJSの両ビルドとminified版を提供。内部プロパティは実際のprivateクラスプロパティに移行し、より厳密なAPI設計を採用。キーと値のnull/undefined制限により予期しないエラーを排除。

主な特徴

  • LRUエビクション: 最も最近使用されていないアイテムの自動削除
  • 高速get操作: get操作に特化した最適化により高速アクセス
  • TypeScript完全対応: TypeScriptで書き直され完全な型安全性を提供
  • メモリ効率: 自動的なサイズ管理による予測可能なメモリ使用量
  • 軽量設計: 最小限の依存関係と高効率な内部実装
  • TTL機能: オプションのTTL(Time To Live)サポート

メリット・デメリット

メリット

  • JavaScript/Node.jsで最高クラスのLRUキャッシュ性能
  • get操作の繰り返しと削除時間最小化に最適化
  • 自動的なメモリ管理によるOOMエラー防止
  • TypeScript完全サポートによる開発生産性向上
  • 長期間の実績による高い安定性と信頼性
  • minified版提供によるバンドルサイズ最適化

デメリット

  • set操作が他の選択肢よりもやや重い(get最適化の代償)
  • Node.js 16.14.0以降が必要(レガシー環境制約)
  • null/undefined値の制限(一部ユースケースで制約)
  • シングルプロセス内キャッシュ(分散キャッシュ不可)
  • LRU以外のエビクション戦略なし
  • 永続化機能なし(プロセス再起動でデータ消失)

参考ページ

書き方の例

インストールと基本セットアップ

# NPM でのインストール
npm install lru-cache

# Yarn でのインストール
yarn add lru-cache

# TypeScript型定義は標準で含まれています(v7以降)

基本的なLRUキャッシュ操作

import { LRUCache } from 'lru-cache'

// LRUキャッシュの作成
const cache = new LRUCache({
  max: 500,               // 最大アイテム数
  maxSize: 5000,          // 最大サイズ
  sizeCalculation: (value, key) => {
    // カスタムサイズ計算
    return JSON.stringify(value).length + key.length
  },
  ttl: 1000 * 60 * 5,     // TTL: 5分(オプション)
  allowStale: false,      // 期限切れアイテムの取得を禁止
  updateAgeOnGet: false,  // get時の年齢更新を無効化
  updateAgeOnHas: false   // has時の年齢更新を無効化
})

// 基本的なset/get操作
cache.set('user:1', { name: 'John', age: 30 })
const user = cache.get('user:1')
console.log(user) // { name: 'John', age: 30 }

// TTL付きでのデータ設定
cache.set('session:abc123', 'session_data', { ttl: 3600000 }) // 1時間

// サイズベースの設定
cache.set('large:data', {
  content: new Array(1000).fill('data'),
  metadata: { created: Date.now() }
})

// 存在確認と削除
if (cache.has('user:1')) {
  console.log('User exists in cache')
}

cache.delete('user:1')
console.log('Cache size:', cache.size)      // 現在のアイテム数
console.log('Cache calculatedSize:', cache.calculatedSize) // 総サイズ

高度なキャッシュ設定と監視

import { LRUCache } from 'lru-cache'

class AdvancedLRUCache {
  constructor() {
    this.cache = new LRUCache({
      max: 1000,
      maxSize: 50000,
      sizeCalculation: this.calculateSize.bind(this),
      ttl: 1000 * 60 * 30,  // 30分のデフォルトTTL
      allowStale: true,      // 期限切れアイテムも返す
      updateAgeOnGet: true,  // アクセス時にage更新
      fetchMethod: this.fetchMethod.bind(this),
      dispose: this.disposeMethod.bind(this),
      noDisposeOnSet: false,
      noUpdateTTL: false
    })

    this.stats = {
      hits: 0,
      misses: 0,
      sets: 0,
      deletes: 0,
      evictions: 0
    }
  }

  calculateSize(value, key) {
    if (typeof value === 'string') {
      return value.length + key.length
    }
    return JSON.stringify(value).length + key.length
  }

  // fetch method: キャッシュミス時の自動データ取得
  async fetchMethod(key, staleValue, { options, signal }) {
    console.log(`Fetching data for key: ${key}`)
    
    if (key.startsWith('user:')) {
      return await this.fetchUserData(key)
    } else if (key.startsWith('api:')) {
      return await this.fetchApiData(key)
    }
    
    return null
  }

  async fetchUserData(key) {
    const userId = key.split(':')[1]
    // 模擬的なデータベースアクセス
    await new Promise(resolve => setTimeout(resolve, 100))
    return {
      id: userId,
      name: `User ${userId}`,
      lastAccess: new Date(),
      fetchedAt: Date.now()
    }
  }

  async fetchApiData(key) {
    // 模擬的なAPI呼び出し
    await new Promise(resolve => setTimeout(resolve, 200))
    return {
      key,
      data: `API data for ${key}`,
      timestamp: Date.now()
    }
  }

  // dispose method: アイテム削除時のクリーンアップ
  disposeMethod(value, key, reason) {
    console.log(`Disposed: ${key} (reason: ${reason})`)
    this.stats.evictions++
    
    // カスタムクリーンアップロジック
    if (key.startsWith('temp:')) {
      console.log(`Cleaning up temporary data: ${key}`)
    }
  }

  // 統計情報付きget
  async get(key) {
    const value = await this.cache.get(key)
    if (value !== undefined) {
      this.stats.hits++
    } else {
      this.stats.misses++
    }
    return value
  }

  // 統計情報付きset
  set(key, value, options = {}) {
    this.stats.sets++
    return this.cache.set(key, value, options)
  }

  // 統計情報付きdelete
  delete(key) {
    const deleted = this.cache.delete(key)
    if (deleted) {
      this.stats.deletes++
    }
    return deleted
  }

  // 統計レポート
  getStats() {
    const total = this.stats.hits + this.stats.misses
    return {
      ...this.stats,
      hitRate: total > 0 ? (this.stats.hits / total * 100).toFixed(2) + '%' : '0%',
      size: this.cache.size,
      calculatedSize: this.cache.calculatedSize,
      remainingTTL: this.cache.getRemainingTTL('user:1')
    }
  }

  // キャッシュクリーンアップ
  prune() {
    const beforeSize = this.cache.size
    this.cache.purgeStale()
    const afterSize = this.cache.size
    console.log(`Pruned ${beforeSize - afterSize} stale entries`)
  }
}

// 使用例
const advancedCache = new AdvancedLRUCache()

// 自動fetch付きでのデータ取得
advancedCache.get('user:123').then(user => {
  console.log('User data:', user)
})

// 定期的な統計レポート
setInterval(() => {
  console.log('Cache Stats:', advancedCache.getStats())
}, 30000)

Express.js でのキャッシュミドルウェア

import express from 'express'
import { LRUCache } from 'lru-cache'

const app = express()

// レスポンスキャッシュの設定
const responseCache = new LRUCache({
  max: 500,
  ttl: 1000 * 60 * 10,  // 10分
  sizeCalculation: (value) => JSON.stringify(value).length,
  maxSize: 1024 * 1024, // 1MB
  allowStale: true
})

// キャッシュミドルウェアの実装
function cacheMiddleware(ttl = 600000) {
  return (req, res, next) => {
    const key = req.originalUrl || req.url
    const cached = responseCache.get(key)

    if (cached) {
      console.log(`Cache HIT: ${key}`)
      return res.json(cached)
    }

    console.log(`Cache MISS: ${key}`)

    // レスポンスのキャプチャ
    const originalJson = res.json
    res.json = function(data) {
      // レスポンスをキャッシュに保存
      responseCache.set(key, data, { ttl })
      console.log(`Cache SET: ${key}`)
      
      originalJson.call(this, data)
    }

    next()
  }
}

// 条件付きキャッシュミドルウェア
function conditionalCache(condition, ttl = 600000) {
  return (req, res, next) => {
    if (!condition(req)) {
      return next()
    }
    
    return cacheMiddleware(ttl)(req, res, next)
  }
}

// APIエンドポイント
app.get('/api/users', cacheMiddleware(300000), async (req, res) => {
  // 重い処理をシミュレーション
  await new Promise(resolve => setTimeout(resolve, 1000))
  
  const users = [
    { id: 1, name: 'John', email: '[email protected]' },
    { id: 2, name: 'Jane', email: '[email protected]' }
  ]
  
  res.json(users)
})

// 条件付きキャッシュ(認証済みユーザーのみ)
app.get('/api/profile', 
  conditionalCache(req => req.headers.authorization, 180000),
  async (req, res) => {
    const profile = {
      id: 1,
      name: 'Current User',
      preferences: {},
      lastLogin: new Date()
    }
    
    res.json(profile)
  }
)

// キャッシュ統計エンドポイント
app.get('/cache/stats', (req, res) => {
  res.json({
    size: responseCache.size,
    calculatedSize: responseCache.calculatedSize,
    maxSize: responseCache.maxSize,
    ttl: responseCache.ttl
  })
})

// キャッシュクリアエンドポイント
app.delete('/cache', (req, res) => {
  const beforeSize = responseCache.size
  responseCache.clear()
  
  res.json({
    message: 'Cache cleared',
    clearedItems: beforeSize
  })
})

app.listen(3000, () => {
  console.log('Server running on port 3000')
})

TTL管理とメモリ最適化

import { LRUCache } from 'lru-cache'

class TTLOptimizedCache {
  constructor() {
    // 短期キャッシュ(1分)
    this.shortTermCache = new LRUCache({
      max: 1000,
      ttl: 60000,
      updateAgeOnGet: true,
      allowStale: false
    })

    // 中期キャッシュ(15分)
    this.mediumTermCache = new LRUCache({
      max: 500,
      ttl: 900000,
      updateAgeOnGet: true,
      allowStale: true
    })

    // 長期キャッシュ(1時間)
    this.longTermCache = new LRUCache({
      max: 100,
      ttl: 3600000,
      updateAgeOnGet: false,
      allowStale: true
    })
  }

  set(key, value, duration = 'medium') {
    switch (duration) {
      case 'short':
        return this.shortTermCache.set(key, value)
      case 'medium':
        return this.mediumTermCache.set(key, value)
      case 'long':
        return this.longTermCache.set(key, value)
      default:
        throw new Error('Invalid duration. Use: short, medium, long')
    }
  }

  get(key) {
    // 短期キャッシュから探す
    let value = this.shortTermCache.get(key)
    if (value !== undefined) {
      return { value, source: 'short' }
    }

    // 中期キャッシュから探す
    value = this.mediumTermCache.get(key)
    if (value !== undefined) {
      // 短期キャッシュに昇格
      this.shortTermCache.set(key, value)
      return { value, source: 'medium' }
    }

    // 長期キャッシュから探す
    value = this.longTermCache.get(key)
    if (value !== undefined) {
      // 中期キャッシュに昇格
      this.mediumTermCache.set(key, value)
      return { value, source: 'long' }
    }

    return { value: undefined, source: 'none' }
  }

  delete(key) {
    return (
      this.shortTermCache.delete(key) ||
      this.mediumTermCache.delete(key) ||
      this.longTermCache.delete(key)
    )
  }

  clear() {
    this.shortTermCache.clear()
    this.mediumTermCache.clear()
    this.longTermCache.clear()
  }

  getMemoryUsage() {
    return {
      short: {
        size: this.shortTermCache.size,
        calculatedSize: this.shortTermCache.calculatedSize
      },
      medium: {
        size: this.mediumTermCache.size,
        calculatedSize: this.mediumTermCache.calculatedSize
      },
      long: {
        size: this.longTermCache.size,
        calculatedSize: this.longTermCache.calculatedSize
      }
    }
  }
}

// 使用例
const tieredCache = new TTLOptimizedCache()

// 異なる期間でのデータ設定
tieredCache.set('session:temp', { data: 'temporary' }, 'short')
tieredCache.set('user:profile', { name: 'John' }, 'medium')
tieredCache.set('config:app', { version: '1.0' }, 'long')

// データ取得と昇格の確認
const result = tieredCache.get('user:profile')
console.log(result) // { value: {...}, source: 'medium' }

TypeScript での型安全な使用

import { LRUCache } from 'lru-cache'

interface User {
  id: number
  name: string
  email: string
  lastLogin?: Date
}

interface CacheConfig {
  maxUsers: number
  userTTL: number
  sessionTTL: number
}

class TypedLRUCache {
  private userCache: LRUCache<string, User>
  private sessionCache: LRUCache<string, string>
  private configCache: LRUCache<string, any>

  constructor(config: CacheConfig) {
    this.userCache = new LRUCache<string, User>({
      max: config.maxUsers,
      ttl: config.userTTL,
      sizeCalculation: (user: User, key: string) => {
        return JSON.stringify(user).length + key.length
      }
    })

    this.sessionCache = new LRUCache<string, string>({
      max: 10000,
      ttl: config.sessionTTL,
      allowStale: false
    })

    this.configCache = new LRUCache<string, any>({
      max: 100,
      ttl: 1000 * 60 * 60 * 24, // 24時間
      updateAgeOnGet: false
    })
  }

  // ユーザーキャッシュ
  setUser(userId: number, user: User): boolean {
    return this.userCache.set(`user:${userId}`, user)
  }

  getUser(userId: number): User | undefined {
    return this.userCache.get(`user:${userId}`)
  }

  // セッションキャッシュ
  setSession(sessionId: string, data: string): boolean {
    return this.sessionCache.set(`session:${sessionId}`, data)
  }

  getSession(sessionId: string): string | undefined {
    return this.sessionCache.get(`session:${sessionId}`)
  }

  // 型安全な設定キャッシュ
  setConfig<T>(key: string, config: T): boolean {
    return this.configCache.set(`config:${key}`, config)
  }

  getConfig<T>(key: string): T | undefined {
    return this.configCache.get<T>(`config:${key}`)
  }

  // 統計情報の型安全な取得
  getStats() {
    return {
      users: {
        size: this.userCache.size,
        calculatedSize: this.userCache.calculatedSize
      },
      sessions: {
        size: this.sessionCache.size,
        calculatedSize: this.sessionCache.calculatedSize
      },
      config: {
        size: this.configCache.size,
        calculatedSize: this.configCache.calculatedSize
      }
    }
  }
}

// 使用例
const typedCache = new TypedLRUCache({
  maxUsers: 1000,
  userTTL: 1800000,  // 30分
  sessionTTL: 3600000 // 1時間
})

const user: User = {
  id: 1,
  name: 'John Doe',
  email: '[email protected]',
  lastLogin: new Date()
}

typedCache.setUser(1, user)
const cachedUser = typedCache.getUser(1) // 型は User | undefined

// 型安全な設定管理
interface AppConfig {
  theme: string
  language: string
  features: string[]
}

const appConfig: AppConfig = {
  theme: 'dark',
  language: 'ja',
  features: ['cache', 'auth', 'logging']
}

typedCache.setConfig<AppConfig>('app', appConfig)
const config = typedCache.getConfig<AppConfig>('app') // 型は AppConfig | undefined