Retour au cours

frontend / javascript

WeakRef et FinalizationRegistry

Leçon 321 exercice

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 WeakRef et 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érenceEmpêche la collecte ?Exemple
Référence normale (variable, propriété)Ouiconst obj = { ... }
WeakMap / WeakSet (clé objet)NonMétadonnées associées à un objet
WeakRefNonCache 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.

js
// --- 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é

  • WeakRef référence un objet sans empêcher sa collecte ; .deref() retourne l'objet ou undefined s'il a déjà été collecté.
  • FinalizationRegistry notifie après une collecte, mais SANS AUCUNE garantie de timing : jamais pour une logique métier critique.
  • Un cache basé sur WeakRef laisse 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 ; WeakRef ne remplace jamais une fermeture explicite.

Exercices pratiques

1 disponible
1

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 :

js
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.

Résoudre l’exercice →