Backend Development 11 min read

Implementation of JSValue in JavaScript Engines: Tagged, Boxing, and Pointer Techniques

The article surveys how major JavaScript engines represent the dynamic JSValue type—using QuickJS’s tagged unions, JavaScriptCore’s NaN‑boxing, SpiderMonkey’s nun‑/pun‑boxing, V8’s tagged pointers with compression, and iOS’s Objective‑C pointer tags—explaining each scheme’s memory layout, performance trade‑offs, and design rationale.

Didi Tech
Didi Tech
Didi Tech
Implementation of JSValue in JavaScript Engines: Tagged, Boxing, and Pointer Techniques

During the development of the Hummer cross‑platform framework, the authors explored the internal representation of JavaScript values (JSValue) in various JavaScript engines. Understanding how a JS engine stores values is essential for performance tuning and avoiding pitfalls in cross‑platform development.

The article first explains that JavaScript is a dynamically‑typed language, where the type information is attached to the runtime value rather than the variable. Consequently, a JSValue must be able to represent several primitive types (undefined, null, boolean, number, reference) and objects.

Implementation approaches for representing JSValue:

tagged representation – used by QuickJS (tagged unions) and V8 (tagged pointers).

boxing representation – includes nan‑boxing (JavaScriptCore) and nun‑/pun‑boxing (SpiderMonkey).

1. Tagged unions (QuickJS)

#else /* !JS_NAN_BOXING */
typedef union JSValueUnion {
    int32_t int32;
    double float64;
    void *ptr;
} JSValueUnion;

typedef struct JSValue {
    JSValueUnion u;
    int64_t tag;
} JSValue;

#define JSValueConst JSValue

This tag‑plus‑struct layout uses a union to reduce memory usage, but each JSValue occupies 16 bytes to keep 8‑byte alignment for both 64‑bit doubles and pointers.

2. Nan‑boxing (JavaScriptCore)

JavaScriptCore follows the IEEE‑754 NaN‑boxing scheme, reserving 51 bits of the NaN payload for custom data. Double values are encoded by adding a constant offset (2^49) so that the resulting bit pattern never starts with 0x0000 or 0xFFFE, allowing the remaining bits to be used for pointers or other types.

ALWAYS_INLINE JSValue::JSValue(EncodeAsDoubleTag, double d)
{
    ASSERT(!isImpureNaN(d));
    u.asInt64 = reinterpretDoubleToInt64(d) + JSValue::DoubleEncodeOffset;
}

inline double JSValue::asDouble() const
{
    ASSERT(isDouble());
    return reinterpretInt64ToDouble(u.asInt64 - JSValue::DoubleEncodeOffset);
}

The engine defines concrete encode patterns for each type, e.g.:

ValEmpty   0x0000 0000 0000 0000
Null       0x0000 0000 0000 0002
False      0x0000 0000 0000 0006
True       0x0000 0000 0000 0007
Undefined  0x0000 0000 0000 000a
Pointer    0x0000 PPPP PPPP PPPP
Double     0x0002 xxxx xxxx xxxx
Integer    0xFFFE 0000 IIII IIII

3. Nun‑boxing & pun‑boxing (SpiderMonkey)

On 32‑bit platforms SpiderMonkey uses nun‑boxing (32‑bit tag + 32‑bit payload). On 64‑bit platforms it switches to pun‑boxing (17‑bit tag + 47‑bit payload) because pointers no longer fit into 32 bits.

4. Tagged pointer (V8)

V8 stores most values as heap objects, but small integers (Smi) and some other primitives are encoded directly in the pointer using tag bits. The layout on 32‑bit and 64‑bit architectures is similar:

Pointer: |_____address_____w1|
Smi:     |___int31_value____0|

To reduce memory consumption on 64‑bit builds, V8 employs pointer compression: pointers are stored as 32‑bit offsets from a base address, halving the size of pointer fields in the heap.

5. Tagged pointer in Objective‑C (iOS)

iOS uses the unused high bits of a 64‑bit pointer to embed a small tag that identifies the object type (e.g., NSString, NSNumber). Example tag definitions:

OBJC_TAG_NSAtom            = 0,
OBJC_TAG_1                 = 1,
OBJC_TAG_NSString          = 2,
OBJC_TAG_NSNumber          = 3,
OBJC_TAG_NSIndexPath       = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate            = 6,
OBJC_TAG_7                 = 7

Summary

Nan‑boxing offers the advantage of keeping double values off the heap, reducing cache pressure and GC work. SpiderMonkey and JavaScriptCore adopt this technique on 64‑bit platforms, while V8 prefers a tagged‑pointer approach with pointer compression to keep pointer size small. Each engine balances memory efficiency, speed, and implementation complexity according to its design goals.

References are provided for further reading on value representation, IEEE‑754, nan‑boxing, SpiderMonkey, V8 pointer compression, and the Objective‑C runtime.

Memory OptimizationJavaScript EngineJSValuenan-boxingTagged Pointer
Didi Tech
Written by

Didi Tech

Official Didi technology account

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.