【问题标题】:JavaScript localStorage cache with size limit and least-recently used (LRU) eviction具有大小限制和最近最少使用 (LRU) 驱逐的 JavaScript localStorage 缓存
【发布时间】:2026-01-10 07:20:04
【问题描述】:

我正在寻找一种在浏览器中执行 Memcached 提供的方法,即能够配置大小限制(例如 localStorage 配额),它会自动逐出旧项目以保持缓存在限制范围内。

好处是不需要显式删除,只要缓存键有版本/时间戳。我见过一些库与“项目数”限制类似,但大小限制对于保持在浏览器的配额之下会更有用。

【问题讨论】:

    标签: javascript caching lru


    【解决方案1】:

    这无法完美实现,因为无法保证浏览器如何存储本地存储的内容,但可以创建足够接近的实现。

    我们可以从 JS 中的字符串是 16-bit unsigned integer value 的事实开始(因为所有内容都作为字符串存储在 localStorage 中)。这意味着我们可以使用content.length * 16 / 8 轻松获取任何字符串的大小(以字节为单位)。

    所以现在我们只需要创建一个缓存来跟踪存储内容的大小和键的大小,它将内容存储在下面。

    一个非常原始和幼稚的实现可能是:

    class Cache {
    
      private keyStoreKey: string;
      private cacheSizeKey: string;
    
      /**
       * List of the keys stored in this cache.
       */
      private storedKeys: string[] = [];
    
      /**
       * The size of the stored values in bytes
       */
      private cacheSize: number = 0;
    
      constructor(
        private prefix: string, 
        private sizeLimit: number,
      ) {
        if(this.sizeLimit < 100) {
          // the minimal size of the cache is 24 + prefix, otherwise, it would enter 
          // a loop trying to remove every element on any insert operation.
          throw new Error('Cache size must be above 100 bytes');
        }
    
        this.keyStoreKey = `${prefix}:k`;
        this.cacheSizeKey = `${prefix}:s`;
    
        this.cacheSize = localStorage.getItem(this.cacheSizeKey) ? Number.parseInt(localStorage.getItem(this.cacheSizeKey)) : 0;
        this.storedKeys = localStorage.getItem(this.keyStoreKey) ? localStorage.getItem(this.keyStoreKey).split(',') : [];
      }
    
      /**
       * The size of the keys in bytes
       */
      public get keyStoreSize() {
        return this.calculateLenght(this.storedKeys.join(`${this.prefix}:v:,`));
      }
    
      public get totalUsedSize() {
        return this.cacheSize + this.keyStoreSize + this.calculateLenght(this.keyStoreKey) + this.calculateLenght(this.cacheSizeKey);
      }
    
      /**
       * Returns the size of the given string in bytes.
       * 
       * The ECMAScript specification defines character as single 16-bit unit of UTF-16 text
       */
      private calculateLenght(content: string): number {
        return content.length * 16 / 8;
      }
    
      /**
       * Saves an item into the cahce.
       * 
       * NOTE: name cannot contain commas.
       */
      public set(name: string, content: string) {
        const newContentSize = this.calculateLenght(content);
    
        if(!(this.storedKeys).some(storedName => storedName === name)) {
          this.storedKeys.unshift(name);
    
          this.cacheSize += newContentSize;
        } else {
          this.storedKeys = this.storedKeys.filter(n => n !== name);
          this.storedKeys.unshift(name);      
    
          const oldContentSize = this.calculateLenght(localStorage.getItem(`${this.prefix}:v:${name}`));
    
          this.cacheSize = this.cacheSize - oldContentSize + newContentSize;
        }
    
        while(this.totalUsedSize > this.sizeLimit && this.storedKeys.length > 0) {
          this.removeOldestItem();
        }
    
        localStorage.setItem(this.cacheSizeKey, this.cacheSize.toString());  
        localStorage.setItem(`${this.prefix}:debug:totalSize`, this.totalUsedSize.toString());  
        localStorage.setItem(this.keyStoreKey, this.storedKeys.join(','));
        localStorage.setItem(`${this.prefix}:v:${name}`, content);
      }
    
      /**
       * The oldest item is the last in the stored keys array
       */
      private removeOldestItem() {
        const name = this.storedKeys.pop();
        const oldContentSize = this.calculateLenght(localStorage.getItem(`${this.prefix}:v:${name}`));
    
        this.cacheSize -= oldContentSize;
    
        localStorage.removeItem(`${this.prefix}:v:${name}`);
      }
    }
    
    window['cache'] = new Cache('test', 200);
    

    我还没有实现读取数据的函数,但是因为键存储在一个数组中,您可以轻松实现 getMostRecent() 或 getNthRecent(position) 或只是一个简单的 get(key) 函数。

    实现在Typescript,如果您不熟悉它,请忽略未知部分。

    【讨论】:

      【解决方案2】:

      如果您想为此功能使用已经存在的解决方案,那么您可以查看这个库 runtime-memcache 在 javascript 中实现 lru 和其他一些缓存方案(mrutimeout)。

      它使用修改后的双向链表来实现 getsetremove 的 O(1)。

      【讨论】:

        最近更新 更多