Maison >interface Web >js tutoriel >Comment créer un cache en mémoire
Dans de nombreux projets, j'ai remarqué que même si un cache peut être pratique, notamment du côté client, il est souvent négligé. La mise en cache côté client est essentielle pour améliorer l'expérience utilisateur en réduisant la latence et en déchargeant les requêtes répétées du serveur. Par exemple, dans les applications avec un défilement infini ou des tableaux de bord fréquemment mis à jour, la mise en cache des données précédemment récupérées évite les appels d'API inutiles, garantissant des interactions plus fluides et des temps de rendu plus rapides.
Dans l'un de mes projets récents, la mise en œuvre d'un cache a réduit le volume d'appels d'API de plus de 40 %, entraînant des améliorations notables des performances et des économies de coûts. Cela souligne pourquoi la mise en cache côté client doit être considérée comme une stratégie d’optimisation fondamentale. Un cache a tendance à être l'une des dernières fonctionnalités considérées, malgré son impact significatif sur les performances avec une mise en œuvre relativement simple, que ce soit en raison de contraintes de temps de développement ou d'autres priorités.
Un cache peut être implémenté à différents niveaux d'architecture : de la mise en cache backend utilisant Redis, un CDN pour le contenu statique, à un cache en mémoire sur le client ou même en utilisant localStorage ou IndexedDB pour la persistance. Idéalement, ces stratégies devraient être combinées pour réduire la charge et le coût des bases de données et des API, ainsi que le décalage des requêtes client-serveur, en particulier pour les données qui ont déjà été récupérées auparavant.
Dans cet article, nous explorerons comment concevoir et implémenter un cache LRU (Least Récemment Utilisé) avec prise en charge TTL (Time-to-Live) en JavaScript, en créant un package similaire à mon adev-lru. À la fin, vous disposerez d'un exemple fonctionnel qui présente les principes et fonctionnalités de base d'une solution de mise en cache efficace.
Un cache LRU (Least Récemment Utilisé) garantit que les éléments les plus récemment consultés restent en mémoire tout en expulsant les moins récemment consultés lorsque leur capacité est dépassée. Cette stratégie fonctionne en maintenant un ordre d'utilisation : chaque accessoire met à jour la position de l'élément dans le cache, les éléments les moins consultés étant supprimés en premier.
Par rapport à d'autres stratégies de mise en cache, LRU équilibre simplicité et efficacité, ce qui la rend bien adaptée aux scénarios dans lesquels l'utilisation récente est un indicateur fiable d'un accès futur. Par exemple, les applications qui mettent en cache les réponses API, les miniatures ou les préférences utilisateur fréquemment consultées peuvent exploiter LRU pour réduire les opérations de récupération redondantes sans trop compliquer le processus d'expulsion.
Contrairement au LFU (Least Frequency Used), qui suit la fréquence d'accès et nécessite une comptabilité supplémentaire, LRU évite cette complexité tout en atteignant d'excellentes performances dans de nombreux cas d'utilisation réels. De même, FIFO (First In, First Out) et MRU (Most Récemment Utilisé) proposent des politiques d'expulsion alternatives, mais peuvent ne pas s'aligner aussi bien sur les modèles d'utilisation où l'activité récente est critique. En combinant LRU avec la prise en charge TTL (Time-to-Live) dans mon implémentation, il gère également les scénarios dans lesquels les données nécessitent une expiration automatique, améliorant encore son applicabilité dans des environnements dynamiques tels que des tableaux de bord en direct ou des services de streaming. C’est particulièrement utile dans les applications où l’accès aux données les plus récentes est essentiel.
La classe LRUCache est conçue pour être efficace, prendre en charge des configurations flexibles et gérer les expulsions automatiques. Vous trouverez ci-dessous quelques méthodes clés :
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
Cette méthode garantit qu'il n'y a qu'une seule instance du cache dans l'application, un choix de conception qui simplifie la gestion des ressources. En implémentant le cache en tant que singleton, nous évitons l'utilisation redondante de la mémoire et garantissons la cohérence des données dans toute l'application. Ceci est particulièrement utile dans les scénarios où plusieurs composants ou modules doivent accéder aux mêmes données mises en cache, car cela évite les conflits et garantit la synchronisation sans nécessiter de logique de coordination supplémentaire. Si aucune capacité n'est spécifiée, la valeur par défaut est 10.
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
Cette méthode ajoute ou met à jour un élément dans le cache. Lorsqu'une clé existe déjà, son élément correspondant est expulsé et réajouté au début du cache. Pour ce faire, le cache utilise une liste doublement liée pour enregistrer les données en tant que nœuds et conserver la possibilité de supprimer les données de la fin de la liste — Tail — et de les déplacer au début de la liste — Head —, pour garantir un O constant. (1) lecture des données de chaque nœud, une table de hachage est utilisée pour enregistrer un pointeur vers chaque nœud de la liste. Ce processus s'aligne sur le principe LRU en garantissant que les éléments récemment consultés sont toujours prioritaires, les marquant ainsi comme « les plus récemment utilisés ». Ce faisant, le cache maintient un ordre d’utilisation précis, ce qui est essentiel pour prendre des décisions d’expulsion lorsque la capacité est dépassée. Ce comportement garantit que les ressources sont gérées de manière optimale, minimisant ainsi le temps de récupération des données fréquemment consultées. Si la clé existe déjà, l'élément est déplacé vers l'avant pour le marquer comme récemment utilisé.
public get(key: string): T | undefined { const node = this.hash.get(key); const now = Date.now(); if (node == null || node.ttl < now) { return undefined; } this.evict(node); this.prepend(node.key, node.value, node.ttl); return node.value; }
Cette méthode récupère les éléments stockés. Si l'élément a expiré, il est supprimé du cache.
Pour évaluer l'efficacité du cache, j'ai implémenté des mesures de performances telles que le taux de réussite, les échecs et les expulsions :
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
Cette méthode efface tous les éléments et réinitialise l'état du cache.
Dans mon implémentation, j'ai également ajouté d'autres méthodes comme getOption qui, au lieu de renvoyer T | undefined, il renvoie une instance de l'option monade pour ceux qui préfèrent une approche plus fonctionnelle. J'ai également ajouté une monade Writer pour suivre chaque opération sur le cache à des fins de journalisation.
Vous pouvez voir toutes les autres méthodes impliquées dans cet algorithme, très bien commentées, sur ce dépôt : https://github.com/Armando284/adev-lru
Un cache LRU n'est pas la seule option. Le choix du bon algorithme de mise en cache dépend fortement des exigences spécifiques de l’application et des modèles d’accès. Vous trouverez ci-dessous une comparaison de LRU avec d'autres stratégies de mise en cache couramment utilisées et des conseils sur le moment d'utiliser chacune d'entre elles :
LRU établit un équilibre entre simplicité et efficacité, ce qui le rend idéal pour les applications où l'activité récente est fortement corrélée à une utilisation future. Par exemple :
En revanche, si les modèles d'accès montrent que la fréquence ou l'ordre d'insertion sont plus pertinents, des algorithmes comme LFU ou FIFO pourraient être un meilleur choix. L'évaluation de ces compromis garantit que la stratégie de mise en cache s'aligne sur les objectifs et les contraintes de ressources de votre application.
La mise en œuvre d'un cache en mémoire peut améliorer considérablement les performances d'une application, en réduisant les temps de réponse et en améliorant l'expérience utilisateur.
Si vous souhaitez voir un cache LRU complet en action, vous pouvez utiliser mon package npm https://www.npmjs.com/package/adev-lru. J'aimerais également recevoir vos commentaires pour continuer à l'améliorer.
Essayez le package et partagez vos réflexions ou contribuez si vous avez envie d'aider davantage ?!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!