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:
dataUser: Stores custom data set by developers via methods like$(elem).data("key", "value").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:
- Lookup or Creation: jQuery checks the element for
the
jQuery.expandoproperty. If it does not exist, jQuery creates it and assigns a unique identifier. - Storage Allocation: jQuery creates an empty object in its internal cache mapped to that identifier.
- 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"):
- jQuery checks the internal
dataUsercache for the requested key. - If the key is not found, jQuery checks the element for a native
attribute named
data-nameusingelement.getAttribute(). - 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). - The parsed value is automatically copied into the internal cache so that subsequent lookups bypass the DOM and read directly from memory.
- Modifying data via
.data("name", "new-value")updates only the internal cache; it intentionally does not write back to the native HTMLdata-*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.