Memory Management in JavaScript with WeakRef and FinalizationRegistry

Syntax overview of WeakRef and FinalizationRegistry

WeakRef

So, let's start with WeakRef. This is a fairly new JavaScript feature (ES2021) that allows you to create a “weak” reference to an object, meaning that if that object is not used anywhere else, the GC can delete it without waiting for all references to be explicitly dropped.

Creating a weak link is super easy:

const weakRef = new WeakRef(targetObject);

Where targetObject – this is any object you want to create a weak reference to.

But it doesn't end there, because WeakRef reveals an important nuance: a weak reference does not protect an object from being deleted by the garbage collector. That is, you can create a reference, but the GC will not spare your object if it is no longer needed. Therefore WeakRef ideal for use cases like caching, where you want to hold onto an object while it's in use, but when it's not needed, it should be freed.

To get an object from a weak reference, there is a method deref():

const obj = weakRef.deref();

Method deref() will return the object if it still exists. If the garbage collector has already collected it, then deref() will return undefined. This is the main trick of weak references – you can't be sure that the object is still in memory.

Here is an example:

let targetObject = { name: "Weak object" };
let weakRef = new WeakRef(targetObject);

// В какой-то момент...
targetObject = null; // Теперь объект доступен только через WeakRef

let obj = weakRef.deref();
if (obj) {
  console.log(`Объект все еще существует: ${obj.name}`);
} else {
  console.log("Объект был удален сборщиком мусора.");
}

Pitfalls

  1. Sync problem: If you thought you could be super smart and create many weak references to one object, keep in mind: after the object is deleted, WeakRef does not guarantee that it can always be restored. Therefore, when caching or other tasks, you need to check that the object still exists after calling deref().

  2. Use with caution: WeakRef It can be useful, but don't overuse it.

FinalizationRegistry

Now let's talk about FinalizationRegistry. This is a registry that allows you to track when objects become unavailable and free up resources asynchronously via callbacks.

First, let's create a registry:

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Объект был финализирован: ${heldValue}`);
});

Here is the callback heldValue will be called when the object is no longer accessible to the code, and JavaScript will delete it. This is very good for freeing up external resources such as file descriptors or sockets.

Now, to register an object in this registry:

registry.register(targetObject, heldValue, unregisterToken);
  • targetObject: the object itself that you are tracking.

  • heldValue: the value that will be passed to the callback when the object is deleted.

  • unregisterToken: This is an optional parameter that allows you to unsubscribe from tracking an object.

Here's what it looks like in practice:

let targetObject = { name: "Tracked object" };
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Объект был финализирован: ${heldValue}`);
});

// Регистрируем объект
registry.register(targetObject, "Мой объект");

// Где-то в коде...
targetObject = null; // Теперь объект доступен только через реестр

When the object is garbage collected, the callback will be called with the passed value. heldValue.

If you need to cancel the registration of an object, simply call:

registry.unregister(unregisterToken);

This unregisterToken — simply a unique identifier for each object that you submitted during registration.

The finalization callback runs asynchronously, so be prepared for the resource release to happen at an unspecified time.

Application

WeakRef for caching

WeakRef — is the perfect tool for creating a cache that automatically frees memory when an object is no longer needed. Imagine you're making a web app that loads a bunch of data from an API, but you don't want that data hanging around in memory forever. A cache with WeakRef will allow you to keep the object in memory, but at the same time will not allow it to clutter up memory if it is no longer used.

Example:

class Cache {
  constructor() {
    this.cache = new Map();
  }

  set(key, value) {
    // Создаем слабую ссылку на объект
    this.cache.set(key, new WeakRef(value));
  }

  get(key) {
    const weakRef = this.cache.get(key);
    if (weakRef) {
      // Получаем объект из слабой ссылки
      const obj = weakRef.deref();
      if (obj) {
        console.log(`Объект по ключу "${key}" найден в кэше.`);
        return obj;
      } else {
        console.log(`Объект по ключу "${key}" был удален сборщиком мусора.`);
        this.cache.delete(key); // Очищаем кэш, если объект был удален
      }
    } else {
      console.log(`Ключ "${key}" не найден в кэше.`);
    }
    return null;
  }
}

// Пример использования:
const cache = new Cache();
let userData = { name: "Alice", age: 30 };

cache.set("user_1", userData);

// Принудительно освобождаем объект
userData = null;

// Пробуем получить объект через кэш
setTimeout(() => {
  const cachedData = cache.get("user_1");
  if (cachedData) {
    console.log(`Данные из кэша: ${cachedData.name}, ${cachedData.age}`);
  } else {
    console.log("Данные были удалены сборщиком мусора.");
  }
}, 1000);

We create a cache that stores weak references to objects. If the object is no longer needed, the GC will remove it from memory, and our cache will update itself. The next time we try to access it, we will be able to understand whether the object has been removed, and if necessary, load it again.

Handling DOM Elements with WeakRef

Another great case for WeakRef — is working with DOM elements that can appear and disappear. Let's say you need to create a SPA application where some components can be temporarily removed from the DOM. Using weak references, you can cache information about DOM elements without worrying that they will remain in memory after being removed from the document.

Example:

class DomCache {
  constructor() {
    this.domElements = new Map();
  }

  setElement(id, element) {
    this.domElements.set(id, new WeakRef(element));
  }

  getElement(id) {
    const weakRef = this.domElements.get(id);
    if (weakRef) {
      const element = weakRef.deref();
      if (element) {
        console.log(`Элемент с ID "${id}" найден в кэше.`);
        return element;
      } else {
        console.log(`Элемент с ID "${id}" был удален сборщиком мусора.`);
        this.domElements.delete(id); // Удаляем из кэша
      }
    } else {
      console.log(`Элемент с ID "${id}" не найден.`);
    }
    return null;
  }
}

// Пример использования:
const domCache = new DomCache();
const divElement = document.createElement("div");
divElement.id = "myDiv";
document.body.appendChild(divElement);

domCache.setElement("myDiv", divElement);

// Удаляем элемент из DOM
document.body.removeChild(divElement);

// Пробуем получить элемент через WeakRef
setTimeout(() => {
  const cachedElement = domCache.getElement("myDiv");
  if (cachedElement) {
    console.log("Элемент найден и все еще существует.");
  } else {
    console.log("Элемент был удален сборщиком мусора.");
  }
}, 1000);

Store a reference to a DOM element in the cache using WeakRefWhen an element is removed from the DOM, it may also be garbage collected, and we can track that.

Freeing Resources with FinalizationRegistry

Now let's move on to the registries. FinalizationRegistry Ideal for tasks where you need to free up resources: closing files, connections, or performing other operations when an object becomes unavailable.

Example:

class FileManager {
  constructor() {
    this.registry = new FinalizationRegistry((fileName) => {
      console.log(`Освобождаем ресурсы для файла: ${fileName}`);
    });
  }

  openFile(fileName) {
    const fileObject = { name: fileName };
    this.registry.register(fileObject, fileName);
    return fileObject;
  }
}

// Пример использования:
const fileManager = new FileManager();
let file = fileManager.openFile("myfile.txt");

// Освобождаем ссылку на файл
file = null;

// Когда сборщик мусора удалит объект, вызовется коллбек и освободит ресурсы.

Created a file, registered it in FinalizationRegistryand when the object became unavailable, the system freed the resources associated with it.

Clearing the Cache with FinalizationRegistry

One of my favorite scenarios is clearing the cache after deleting an object.

Example:

class ObjectCache {
  constructor() {
    this.cache = new Map();
    this.registry = new FinalizationRegistry((key) => {
      console.log(`Объект с ключом "${key}" был удален. Очищаем кэш.`);
      this.cache.delete(key);
    });
  }

  setObject(key, obj) {
    this.cache.set(key, obj);
    this.registry.register(obj, key);
  }

  getObject(key) {
    return this.cache.get(key);
  }
}

// Пример использования:
const cache = new ObjectCache();
let obj = { name: "Cache me if you can" };

cache.setObject("obj_1", obj);

// Освобождаем ссылку
obj = null;

// Когда объект будет удален сборщиком мусора, кэш будет автоматически очищен.

Created a cache and registered objects in FinalizationRegistryWhen an object becomes unavailable, the registry takes care of removing it from the cache.

Conclusion

That's how things are with WeakRef And FinalizationRegistry. If used correctly, they can significantly improve memory management and avoid leaks in complex applications. Caching, working with DOM elements, freeing file descriptors and network connections – and more, all this is now under your control.

Well, is it time to apply these things in production?


In conclusion, I will say a few words about the open lesson dedicated to creating a RestFull API with NestJs, which will be held on September 24. As a result of participating in it, you will learn how to create a scalable API using modern frameworks. If you are interested – sign up using the link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *