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.
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 JSValueThis 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 IIII3. 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 = 7Summary
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.
Didi Tech
Official Didi technology account
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.