How V8 Optimizes Object Properties: From TaggedImpl to Slack Tracking
This article explains V8's internal object representation, pointer tagging, the three property storage modes (inobject, fast, slow), how constructors and literals allocate space, the rules that trigger mode switches, and the slack‑tracking technique that refines memory usage.
Object Representation in V8
All heap‑managed entities in V8 inherit from TaggedImpl. V8 uses a precise garbage collector and pointer tagging to distinguish small integers (Smi) from heap objects: the least‑significant bit of an address is 0 for Smis and 1 for heap objects.
Object Layout and Field Offsets
The runtime memory layout consists of a native stack, the GC heap, and three pointers stored in the object header: map, propertiesOrHash, and elements. The inheritance chain is:
Object → HeapObject → JSReceiver → JSObject
Each subclass adds its own offset fields ( kMapOffset, kPropertiesOrHashOffset, kElementsOffset). Offsets are defined in field‑offsets‑tq.h and are calculated from the header size and kTaggedSize (8 bytes on 64‑bit platforms).
Property Storage Modes
Named properties can be stored in three mutually exclusive forms:
inobject – the value pointer resides directly in the object's contiguous memory; this is the fastest access path.
fast – the value is kept in a PropertyArray; an extra indirection via the propertiesOrHash pointer is required.
slow – the property is stored in a generic NameDictionary; inline caches cannot be used, making it the slowest.
Lookup Process
Search the object's DescriptorArray for the property name to obtain an index.
Combine the object's base address with the index (and the appropriate pointer) to retrieve the value.
Object Creation
From Constructors
When a constructor is compiled, V8 estimates the number of properties ( estimated property count ) and adds a fixed slack of eight slots. The initial map stores expected_nof_properties. Example:
function Ctor1() { this.p1 = 1; this.p2 = 2; }
const o1 = new Ctor1();
%DebugPrint(o1);The debug output shows a map with inobject properties: 10 (2 estimated + 8 slack). Empty constructors receive the same default of ten inobject slots.
From Object Literals
Object literals allocate exactly the number of properties present in the literal; no slack is added. An empty literal receives a hard‑coded initial count of four slots, derived from kInitialGlobalObjectUnusedPropertiesCount. Using Object.create(null) creates a slow object directly.
Mode Transitions
If inobject space is sufficient, new properties stay inobject.
When the inobject quota is exceeded, properties move to fast.
If the fast quota (soft limit 12, hard limit 15) is exceeded, the whole object switches to slow.
Deleting any non‑last property or assigning the object as another function's prototype forces a transition to slow.
After a slow transition the object rarely returns to fast, except for rare internal migrations.
Slack Tracking
Slack tracking refines the initially over‑allocated inobject space. Each constructor has a construction_counter starting at seven. After the counter reaches zero, V8 recomputes the final instance_size based on the actual number of observed properties, reducing wasted memory.
Enabling Native Debug APIs
Running V8 with --allow-natives-syntax enables private APIs such as %DebugPrint, which prints an object's map, instance size, and property layout. Example command:
node --allow-natives-syntax test.jsIn‑Depth Example of Constructor‑Based Creation
Consider the following constructors:
function Ctor1() { this.p1 = 1; this.p2 = 2; }
function Ctor2(condition) {
this.p1 = 1;
this.p2 = 2;
if (condition) { this.p3 = 3; this.p4 = 4; }
}
const o1 = new Ctor1();
const o2 = new Ctor2(true);
%DebugPrint(o1);
%DebugPrint(o2);The debug output for Ctor1 shows inobject properties: 10 (2 real properties + 8 slack). For Ctor2 the estimated count is 4, so the map records inobject properties: 12 (4 + 8). If the constructor adds more properties at runtime, they will first occupy the pre‑allocated slack; once the slack is exhausted the object transitions to fast.
Empty Constructors
Even an empty constructor gets inobject properties: 10. V8 assumes most constructors will have at least two properties, so it adds a default of two before the eight‑slot slack.
Classes
ES6 classes are syntactic sugar for constructor functions. The same inobject estimation applies. For example:
class C { p1 = 1; p2 = 2; p3 = 3; }
const o = new C();
%DebugPrint(o);The map reports inobject properties: 11 (3 declared + 8 slack). When class fields are compiled with Object.defineProperty (as produced by Babel or TypeScript with useDefineForClassFields), the engine may not count them in the estimated property count, causing some fields to fall back to fast storage.
Object Literals
Literal creation uses the exact property count. Example:
const a = { p1: 1 };
%DebugPrint(a);The debug output shows inobject properties: 1. An empty literal ( {}) receives four pre‑allocated slots because Genesis::CreateObjectFunction sets kInitialGlobalObjectUnusedPropertiesCount = 4. Creating an object with Object.create(null) bypasses the fast path and creates a slow object directly.
Inobject, Fast, and Slow Limits
On 64‑bit V8 without pointer compression, the maximum number of inobject properties is calculated as:
constexpr int kSystemPointerSize = sizeof(void*);
constexpr int kTaggedSize = kSystemPointerSize;
constexpr int kJSObjectHeaderSize = 3 * kTaggedSize;
constexpr int kMaxInstanceSize = 255 * kTaggedSize;
constexpr int kMaxInObjectProperties = (kMaxInstanceSize - kJSObjectHeaderSize) >> 3; // 252Fast properties are stored in a PropertyArray that grows in steps of kFieldsAdded = 3. The soft limit is 12, the hard limit is 15 (checked by Map::TooManyFastProperties).
Practical Tests
Adding properties to a plain object demonstrates the transitions:
const obj = {};
for (let i = 0; i < 19; ++i) obj['p' + i] = 1;
%DebugPrint(obj);With 4 properties the object uses FixedArray[0] (inobject). At 19 properties the map shows a PropertyArray[15] (fast). At 20 properties the map switches to a NameDictionary[101] (slow).
Summary of Key Points
V8 objects have three storage modes: inobject (fastest), fast , and slow (slowest).
Constructor‑based creation allocates estimated properties + 8 slack inobject slots (up to 252 total).
Object literals allocate exactly the literal's property count (four slots for an empty literal).
When inobject space is exhausted, properties move to fast; exceeding fast limits (soft 12, hard 15) converts the whole object to slow.
Deleting non‑last properties or using an object as another function's prototype forces a slow transition.
Slack tracking reduces over‑allocation by adjusting instance_size after a constructor has been invoked seven times.
Using --allow-natives-syntax and %DebugPrint is a convenient way to inspect maps, offsets, and property storage during experimentation.
References
A tour of V8: object representation – https://www.jayconrod.com/posts/52/a-tour-of-v8--object-representation
Fast properties in V8 – https://v8.dev/blog/fast-properties
Pointer compression in V8 – https://v8.dev/blog/pointer-compression
Slack tracking in V8 – https://v8.dev/blog/slack-tracking
Inline caching – https://en.wikipedia.org/wiki/Inline_caching
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
