How jQuery Caches Data on DOM Elements Internally

jQuery manages data associated with DOM elements through an internal abstraction layer rather than attaching JavaScript objects directly to native DOM nodes. This design prevents severe memory leaks caused by circular references in older browser engines and ensures performant, decoupled data storage. By using a unique identifier on the DOM element that points to an internal storage registry, jQuery seamlessly links arbitrary data and event handlers to elements while safely handling cleanup when elements are removed from the document.

The Memory Leak Problem

In early browser versions (most notably Internet Explorer 6–8), creating circular references between native DOM elements and JavaScript objects caused the garbage collector to fail, leaking memory until the tab or browser closed. A direct assignment such as element.myData = { target: element } formed a circular loop.

To resolve this, jQuery introduced an internal data cache. Instead of placing rich JavaScript objects directly on the element, jQuery stores the data in a central JavaScript object and only attaches a primitive identifier or lightweight private property to the DOM node itself.

The Expando Identifier

When you use .data() on an element for the first time, jQuery marks the DOM node with a custom property known as an "expando".

The expando is a dynamically generated string designed to be unique to the running jQuery instance, typically structured like:

jQuery.expando = "jQuery" + (version + Math.random()).replace(/\D/g, "");

In older versions of jQuery (1.x and 2.x), this property contained a unique integer ID (an index). In modern jQuery (3.x and later), the expando holds a unique key representing a slot in a private Data object instance.

The Internal Data Architecture

jQuery maintains two internal data stores created via an internal Data constructor:

  1. dataUser: Stores custom data set by developers via methods like $(elem).data("key", "value").
  2. dataPriv: Stores private internal data, such as event listeners attached via .on() and animation queues.

When a write operation occurs, jQuery executes the following steps:

  1. Lookup or Creation: jQuery checks the element for the jQuery.expando property. If it does not exist, jQuery creates it and assigns a unique identifier.
  2. Storage Allocation: jQuery creates an empty object in its internal cache mapped to that identifier.
  3. Data Assignment: The requested key and value are saved to the internal object, completely isolated from the native DOM node.

Handling HTML5 data-* Attributes

jQuery's .data() method bridges the gap between HTML5 data attributes and JavaScript data caching. When retrieving data via .data("name"):

  1. jQuery checks the internal dataUser cache for the requested key.
  2. If the key is not found, jQuery checks the element for a native attribute named data-name using element.getAttribute().
  3. If an HTML5 attribute exists, jQuery attempts to parse the string value into its native JavaScript type (such as booleans, numbers, null, or JSON objects/arrays).
  4. The parsed value is automatically copied into the internal cache so that subsequent lookups bypass the DOM and read directly from memory.
  5. Modifying data via .data("name", "new-value") updates only the internal cache; it intentionally does not write back to the native HTML data-* attribute for performance reasons.

Cleanup and Garbage Collection

Detached elements can easily cause memory leaks if their associated cache entries persist. jQuery prevents this through its DOM manipulation methods (such as .remove(), .empty(), and .html()).

Before an element is detached, jQuery routes it through an internal function named cleanData(). This function reads the element's jQuery.expando property, tears down all bound event handlers, and deletes the associated entries from both the private and user data caches. Once the reference in the cache is deleted, the JavaScript engine can safely reclaim the memory.