frontend / javascript
WeakRef et FinalizationRegistry
Explication
Ce que vous allez apprendre
- Expliquer la différence entre une référence forte et une référence faible
- Créer un
WeakRefet récupérer son contenu avec.deref() - Enregistrer un callback de nettoyage avec
FinalizationRegistry - Justifier pourquoi ce callback ne doit jamais porter de logique métier critique
- Reconnaître un bon cas d'usage (cache mémoire) d'un mauvais (fermeture de connexion)
Dans quel contexte ?
Un développeur construit un cache d'objets coûteux à recalculer (des images redimensionnées, par exemple) et hésite entre une Map classique et une structure à base de WeakRef. Avec une Map classique, chaque entrée reste en mémoire indéfiniment tant qu'elle n'est pas explicitement supprimée, ce qui finit par saturer la mémoire de l'application sur une longue session. Avec un cache basé sur WeakRef, les entrées dont l'objet source n'est plus utilisé ailleurs dans le programme sont automatiquement libérées par le garbage collector, sans code de nettoyage explicite à maintenir.
Rappel : comment JavaScript décide qu'un objet peut être libéré
Le moteur JavaScript garde en mémoire tout objet tant qu'au moins une référence "forte" y mène (une variable, un tableau, une propriété d'objet). Dès qu'aucune référence forte ne subsiste, le garbage collector est libre de le libérer, à un moment qu'il choisit lui-même — jamais garanti, jamais immédiat. WeakMap et WeakSet (vus dans une leçon précédente) exploitent déjà ce principe pour des clés objets ; WeakRef et FinalizationRegistry vont plus loin.
Prérequis
Cette dernière leçon suppose la leçon 19 sur le garbage collector bien assimilée : le principe d'atteignabilité (un objet collecté dès qu'il n'est plus référencé par une racine) est la base indispensable pour comprendre WeakRef.
WeakRef : détenir une référence sans empêcher la collecte
Une référence normale "retient" son objet en vie. WeakRef fait l'inverse : elle enveloppe un objet sans empêcher le garbage collector de le libérer. Pour récupérer l'objet (s'il existe encore), on appelle .deref(), qui renvoie soit l'objet, soit undefined s'il a déjà été collecté. C'est utile pour construire un cache qui ne provoque jamais de fuite mémoire : les entrées non utilisées ailleurs disparaissent naturellement, sans code de nettoyage explicite.
| Type de référence | Empêche la collecte ? | Exemple |
|---|---|---|
| Référence normale (variable, propriété) | Oui | const obj = { ... } |
WeakMap / WeakSet (clé objet) | Non | Métadonnées associées à un objet |
WeakRef | Non | Cache mémoire auto-nettoyant |
FinalizationRegistry : être notifié après coup, pas avant
FinalizationRegistry permet d'enregistrer une fonction de rappel exécutée après qu'un objet a été collecté — par exemple pour logguer ou libérer une ressource externe associée (fichier, connexion). Mais attention : ce callback n'a aucune garantie de timing, ni même d'exécution (le programme peut se terminer avant qu'il ne soit appelé).
Pourquoi ces outils sont dangereux à mal utiliser
Comme le comportement du garbage collector est non déterministe par conception, il ne faut jamais baser une logique métier (fermer une connexion critique, libérer un verrou) sur le déclenchement de FinalizationRegistry. Ces API sont réservées à des optimisations de mémoire (caches, diagnostics), jamais à de la logique dont dépend la correction du programme.
Piège fréquent
Fermer une connexion à une base de données dans le callback d'un FinalizationRegistry est une erreur grave : ce callback peut s'exécuter des secondes, voire des minutes après que l'objet est devenu inatteignable, ou ne jamais s'exécuter si le programme se termine avant. Une ressource critique doit toujours être libérée explicitement (méthode close(), bloc finally), jamais via ce mécanisme.
Commandes & code
WeakRef et FinalizationRegistry
Références qui n'empêchent pas la collecte, et notification de nettoyage — à manier avec précaution.
// --- WeakRef : contient une reference qui NE MAINTIENT PAS l'objet en vie ---
let utilisateur = { nom: "Alice" };
const referenceFaible = new WeakRef(utilisateur);
console.log(referenceFaible.deref()); // { nom: "Alice" } -- tant que "utilisateur" existe encore
utilisateur = null; // plus aucune reference FORTE vers l'objet
// A un moment INDETERMINE (decide par le moteur), le GC peut collecter l'objet.
// console.log(referenceFaible.deref()); // peut devenir "undefined", sans garantie de delai
// --- Difference avec WeakMap/WeakSet : WeakRef s'utilise sur UNE valeur isolee ---
// WeakMap/WeakSet exigent des CLES objets ; WeakRef enveloppe directement UNE reference.
// --- Cas d'usage n°1 : cache qui n'empeche pas la collecte des entrees non utilisees ---
class CacheFaible {
#cache = new Map(); // cle -> WeakRef(valeur)
obtenir(cle, fabrique) {
const ref = this.#cache.get(cle);
const valeurExistante = ref?.deref();
if (valeurExistante !== undefined) {
return valeurExistante; // reutilise l'objet encore vivant
}
const nouvelleValeur = fabrique();
this.#cache.set(cle, new WeakRef(nouvelleValeur));
return nouvelleValeur;
}
}
const cacheImages = new CacheFaible();
// Si plus personne d'autre ne retient l'image decodee, le GC peut la liberer :
// le cache ne provoque alors AUCUNE fuite memoire, contrairement a une Map classique.
// --- FinalizationRegistry : etre notifie APRES la collecte d'un objet ---
const registre = new FinalizationRegistry((valeurNettoyage) => {
console.log(`Objet collecte, nettoyage associe : ${valeurNettoyage}`);
});
function creerRessource(id) {
const ressource = { id, gros_buffer: new ArrayBuffer(1024 * 1024) };
registre.register(ressource, `ressource-${id}`); // 2e argument : donnee passee au callback
return ressource;
}
let r = creerRessource(1);
r = null;
// Le callback peut s'executer plus tard, voire JAMAIS si le programme se termine avant --
// AUCUNE garantie de timing. Ne JAMAIS baser une logique metier dessus.
// --- unregister : annuler l'enregistrement si l'objet est nettoye manuellement avant ---
const jetonDesenregistrement = {};
function creerRessourceAvecNettoyageManuel(id) {
const ressource = { id };
registre.register(ressource, `ressource-${id}`, jetonDesenregistrement);
return {
ressource,
fermer() {
registre.unregister(jetonDesenregistrement); // evite un double-nettoyage
console.log(`Ressource ${id} fermee explicitement`);
},
};
}
// --- Regle absolue : WeakRef/FinalizationRegistry ne sont PAS un remplacement de try/finally ---
// MAUVAIS : compter sur le GC pour liberer une ressource critique (fichier, connexion, verrou)
class ConnexionMauvaisePratique {
constructor() {
this.ouverte = true;
registre.register(this, "connexion-non-fermee", this);
// Le GC pourrait ne JAMAIS passer avant la fin du programme : fuite de ressource garantie
}
}
// BON : liberer explicitement via un pattern deterministe (voir la lecon Debounce/Throttle,
// ou "using" -- explicit resource management, ES2024)
class ConnexionCorrecte {
#ouverte = true;
constructor() { this.#ouverte = true; }
fermer() {
this.#ouverte = false;
console.log("Connexion fermee de facon deterministe");
}
[Symbol.dispose]() {
this.fermer(); // integration avec "using" (ES2024, cf. TC39 stage 4)
}
}
// using connexion = new ConnexionCorrecte();
// -- fermer() est appele AUTOMATIQUEMENT et IMMEDIATEMENT a la sortie du bloc, sans GC
// --- Observer une structure DOM sans empecher sa collecte (cas d'usage reel) ---
function observerSansRetenir(element, callback) {
const ref = new WeakRef(element);
const intervalId = setInterval(() => {
const el = ref.deref();
if (!el || !document.contains(el)) {
clearInterval(intervalId); // l'element a disparu du DOM (ou a ete collecte)
return;
}
callback(el);
}, 1000);
}Résumé
WeakRefréférence un objet sans empêcher sa collecte ;.deref()retourne l'objet ouundefineds'il a déjà été collecté.FinalizationRegistrynotifie après une collecte, mais SANS AUCUNE garantie de timing : jamais pour une logique métier critique.- Un cache basé sur
WeakReflaisse le GC libérer les entrées inutilisées, évitant la fuite d'un cache non borné classique. - La libération déterministe de ressources (
try/finally,using) reste indispensable ;WeakRefne remplace jamais une fermeture explicite.
Exercices pratiques
Mission : auditer un cache et une connexion basés sur le GC
Objectif : Corriger une mauvaise utilisation de FinalizationRegistry pour une ressource critique, et combler une fuite silencieuse dans un cache à base de WeakRef.
Contexte
Un développeur écrit un cache d'images avec WeakRef, comme dans le cours, mais aussi une classe ConnexionBaseDeDonnees :
class ConnexionBaseDeDonnees {
constructor() {
this.ouverte = true;
registre.register(this, "fermeture-connexion", this);
// pense que ca suffit a fermer la connexion proprement le moment venu
}
}Par ailleurs, dans CacheFaible, chaque clé insérée dans la Map interne y reste indéfiniment, même quand la valeur associée a été collectée par le GC : seule la référence à l'intérieur (le WeakRef) devient inutile, mais la Map elle-même ne cesse jamais de grossir.