cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Hidden classes & object shapes

Learn how JavaScript engines use hidden classes, transition trees, descriptor arrays, fast properties, and migrations to speed up objects.

By the end, you can
  • 01
    Explain object layoutsDescribe how maps, shapes, or structures turn dynamic property names into fixed offsets that optimized code can trust after a guard.
  • 02
    Read transitions and storageTrace property-order transitions, descriptor arrays, in-object fields, fast-property backing stores, and dictionary mode.
  • 03
    Use engine facts safelyPredict what delete, Object.freeze, slack tracking, and representation changes do in V8 12.4 without confusing internals with JavaScript semantics.

Dynamic objects can still get a fixed layout

JavaScript lets you add, update, delete, freeze, and reorder object properties at runtime. That flexibility looks hostile to fast machine code: how can an engine load user.name quickly if every object can be a different bag of names?

Definition

A hidden class is an engine's internal layout record for an object. It says which property names exist, what order they were added in, and where their values live, so a checked object can be read by fixed offsets instead of a full search.

V8 calls these records maps. SpiderMonkey talks about shapes. JavaScriptCore talks about structures. The names differ, but the job is similar: give dynamic objects a stable layout that inline caches and optimizers can guard. The Inline caches lesson introduced the guard; here we inspect the layout behind that guard.

We will stay honest about boundaries. JavaScript semantics come from the language: property lookup, descriptors, prototypes, freezing, and deletion. Hidden classes are implementation details. The Node-only probes use V8 12.4 diagnostics; other engines use their own data structures and can change details between releases.

Maps, shapes, and structures

Start with a tiny clue you can see in normal JavaScript: property insertion order. The three tickets below have the same names, but Meera's object inserts platform before name. Browsers do not expose hidden classes, so this snippet only prints keys. The Node probe after it checks V8 maps directly.

Same names, same order; same names, different orderPop out in the code editor (opens in a new tab)JavaScript
function makeTicket(name) {  return { name, platform: 2, paid: true };} const asha = makeTicket("Asha");const ravi = makeTicket("Ravi");const meera = { platform: 2, name: "Meera", paid: true }; console.log(Object.keys(asha).join(","));console.log(Object.keys(ravi).join(","));console.log(Object.keys(meera).join(","));
Node-only %HaveSameMap proofJavaScript
function Point(x) {  this.x = x;} const a = { x: 1, y: 2 };const b = { x: 3, y: 4 };const c = { y: 2, x: 1 };const p = new Point(1);const q = new Point(2); console.log("node", process.version);console.log("v8", process.versions.v8);console.log("same order", %HaveSameMap(a, b));console.log("different order", %HaveSameMap(a, c));console.log("constructor start", %HaveSameMap(p, q));p.y = 2;console.log("after one transition", %HaveSameMap(p, q));q.y = 3;console.log("after both transition", %HaveSameMap(p, q));
Verified runtime fact

The lesson test runs this in Node 22 with V8 12.4. Same property order reports true, different order reports false, and two constructor instances diverge when only one takes an added-property transition, then match after both take it.

Engine names for the layout record
EngineCommon nameWhat to remember
V8Map, also called a hidden classFirst pointer in the object; compares layouts quickly and points at descriptors and transitions.
SpiderMonkeyShapeRepresents property layout information used by property lookup and inline caches.
JavaScriptCoreStructureJSC's object-layout structure used by its property access and speculation machinery.

Primary sources use these names too: the V8 maps documentation introduces maps, descriptor arrays, and transition arrays; Fast properties in V8 explains named properties and elements; and Mathias Bynens and Benedikt Meurer's Shapes and Inline Caches article compares the terminology across engines.

Transition chains and trees

A new empty object starts at a root layout. Adding a new property follows an edge to another layout. Add name, then score, and you reach one map. Add score, then name, and you reach a sibling map. That is why property order matters even when the final set of names matches.

Real-life analogyA school uniform and name badge

A school uniform tells a teacher where to find each badge. If the same badges appear in a different order, the teacher needs a different layout to find them quickly.

In real life: A uniform has a name-badge place
In JavaScript: The root map for a new object
In real life: Name badge, then house badge
In JavaScript: A transition chain for name, then course
In real life: House badge, then name badge
In JavaScript: A sibling branch with a different property order
In real life: Extra badges make placement less fixed
In JavaScript: Dictionary mode after heavy property churn

Where the analogy stops: A school chooses uniform rules deliberately. Engines build maps while your code runs and still honor JavaScript property rules.

Step through a hidden-class transition chain
Step 0 of 9Ready
Your turn: follow the blue line

Follow three objects through a teaching-model transition tree. This records real JavaScript model calls, not browser engine memory.

Running in
  1. script
Next: line 18
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const transitions = new Map();function nextMap(current, property) {  const key = current + "->" + property;  if (!transitions.has(key)) transitions.set(key, current + "." + property);  return transitions.get(key);} function makeRecord(label) {  return { label, map: "Root", slots: {} };} function store(record, property, value) {  record.map = nextMap(record.map, property);  record.slots[property] = value;  return record;} store(asha, "name", "Asha");store(asha, "score", 10);const ravi = makeRecord("Ravi");store(ravi, "name", "Ravi");store(ravi, "score", 20);const meera = makeRecord("Meera");store(meera, "score", 30);store(meera, "name", "Meera"); console.log(asha.map === ravi.map);console.log(asha.map === meera.map);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Line 19 and line 20 put Asha on the name → score chain. Ravi follows the same chain on lines 22 and 23, so line 28 prints true. Meera takes score → name on lines 25 and 26, so line 29 prints false.

Playground: build a transition tree
Teaching-model transition treeJavaScript
const asha = objectWith(["x", "y"]);const ravi = objectWith(["x", "y"]);const meera = objectWith(["y", "x"]); console.log(asha.map === ravi.map);console.log(asha.map === meera.map);console.log(treeFromRoot());
Map tree0 objects

No objects yet. Add a property to the draft to create the first transition.

  1. rootM0

    no descriptors yet

Step 0 of 41 maps

Add x, y, or z to the draft object. Save it, then build another object in the same or a different order.

This is a teaching model; it does not inspect your browser's engine. It rebuilds a small map tree from the object orders you choose.

Descriptor arrays: the property list behind maps

A map does not only say “this object has a shape.” It points at descriptors: metadata for each named property, such as the property name, attributes, representation, and field offset. V8's docs call this structure a DescriptorArray. Transitional maps can share a descriptor array and use a descriptor count as a prefix.

Node-only proof that a chain shares a descriptor arrayJavaScript
const one = {};one.x = 1;const two = {};two.x = 1;two.y = 2; console.log("ONE");%DebugPrint(one);console.log("TWO");%DebugPrint(two);
Verified runtime fact

The test parses the two %DebugPrint listings and checks that the parent and child maps mention the same DescriptorArray[2] address within that one run. It never asserts the address itself.

That shared array is why the word “hidden class” can mislead. The layout record is not a JavaScript class body. It is a compact engine data structure that lets several related maps agree on property metadata while each map represents a different point in the tree.

In-object, fast, and slow properties

V8 separates named properties from array elements. Named properties can live directly inside the object body, or in a separate properties backing store. Both are still “fast properties” when the map and descriptors tell the engine where to look. Slow or dictionary properties trade fixed offsets for flexible updates.

Normal JavaScript can show the names, not the storagePop out in the code editor (opens in a new tab)JavaScript
const fast = { name: "Asha", score: 0, active: true };const extended = { name: "Ravi", score: 0, active: true };extended.city = "Pune"; console.log(Object.keys(fast).join(","));console.log(Object.keys(extended).join(","));console.log("city" in extended);
Where named properties can live
RepresentationWhat it meansWhy engines use it
In-object propertyThe value lives inside the object body at a fixed field offset.Fastest common case, but the object cannot grow in place forever.
Fast property backing storeThe map still describes offsets, but extra values live in a properties array beside the object.Still fast because descriptors map names to indexes.
Dictionary propertyProperties move to a hash-table-like dictionary with metadata beside each entry.Flexible for deletes or many dynamic names, but slower than fixed offsets.
Node-only %HasFastProperties storage probeJavaScript
const removedMiddle = { a: 1, b: 2, c: 3 };console.log("fast before delete", %HasFastProperties(removedMiddle));delete removedMiddle.b;console.log("fast after delete middle", %HasFastProperties(removedMiddle)); const removedLast = { a: 1, b: 2, c: 3 };delete removedLast.c;console.log("fast after delete last", %HasFastProperties(removedLast)); const many = {};for (let i = 0; i < 80; i += 1) many["p" + i] = i;console.log("fast after many", %HasFastProperties(many)); const frozen = { a: 1, b: 2 };console.log("fast before freeze", %HasFastProperties(frozen));Object.freeze(frozen);console.log("fast after freeze", %HasFastProperties(frozen));console.log("is frozen", Object.isFrozen(frozen));
Verified runtime fact

The test proves that a fresh object is fast, deleting a middle or last named property is not fast in this V8 12.4 build, 80 dynamic properties are not fast, and a frozen two-property object can still have fast properties.

Slack tracking trims early extra space

V8 gives new constructor instances a little extra in-object space at first. That is slack tracking: the engine leaves room in case later initialization code adds more fields, then trims unused space after enough constructions. The V8 slack-tracking post describes a construction counter initialized around seven and an extra-property allowance used during this settling period.

Real-life analogyA new notebook leaves extra blank lines

A new notebook leaves a few blank lines in case you need more notes. After a few pages, you know the usual amount and stop leaving space.

In real life: The first page has blank lines
In JavaScript: Early maps reserve unused in-object fields
In real life: Later pages use only needed lines
In JavaScript: Slack tracking trims unused fields
In real life: Written lines keep their places
In JavaScript: Existing property offsets remain usable
In real life: Extra notes need another page
In JavaScript: Extra properties can move to the backing store

Where the analogy stops: A person chooses notebook space. V8 uses constructor counts and heuristics, and exact numbers are engine details.

Node-only slack-tracking probeJavaScript
function Seat(name) {  this.name = name;  this.paid = true;} const first = new Seat("Asha");console.log("FIRST");%DebugPrint(first); for (let i = 0; i < 12; i += 1) new Seat("warm" + i);const later = new Seat("Ravi");console.log("LATER");%DebugPrint(later);console.log("same map after trim", %HaveSameMap(first, later));
Verified runtime fact

The test parses %DebugPrint before and after warm-up. The first map has more in-object fields and unused property fields; the later map has fewer in-object fields, zero unused fields, and %HaveSameMap(first, later) is still true after trimming.

Dictionary mode, `delete`, and `freeze`

Dictionary mode is not a punishment. It is the representation an engine can choose when fixed offsets are no longer the best fit. Deleting named properties creates holes in the descriptor-based layout. Adding many unpredictable names also starts to look like a dictionary workload. Freezing is different: it changes mutability and extensibility, but V8 can keep fast storage for small completed objects.

Language behavior: delete removes, freeze locksPop out in the code editor (opens in a new tab)JavaScript
const row = { id: 1, name: "Asha", city: "Pune" };delete row.city; const settings = { theme: "dark", compact: false };Object.freeze(settings); console.log(Object.keys(row).join(","));console.log(Object.isFrozen(settings));console.log(settings.theme);
Node-only delete and freeze map probeJavaScript
const removed = { id: 1, name: "Asha", city: "Pune" };delete removed.city;console.log("delete stays fast", %HasFastProperties(removed)); const settings = { theme: "dark", compact: false };console.log("freeze before", %HasFastProperties(settings));Object.freeze(settings);console.log("freeze after", %HasFastProperties(settings));console.log("frozen map differs", %HaveSameMap({ theme: "dark", compact: false }, settings));
Node-only DebugPrint dictionary and frozen-map excerpt producerJavaScript
const dictionary = { a: 1, b: 2, c: 3 };delete dictionary.b;console.log("DICTIONARY");%DebugPrint(dictionary); const frozen = { a: 1, b: 2 };Object.freeze(frozen);console.log("FROZEN");%DebugPrint(frozen);
What delete and freeze did in the V8 12.4 probes
OperationShape effectMeasured fact
delete obj.middleIn V8 12.4, the object switches to dictionary properties.%HasFastProperties becomes false in the lesson probe.
delete obj.lastThis Node/V8 build also switches to dictionary properties.Do not rely on a last-property rollback folklore rule.
Add many names dynamicallyV8 may choose dictionary mode after enough dynamic additions.The probe uses 80 names and sees slow properties.
Object.freeze(obj)V8 creates a non-extensible frozen map, but the object can stay fast.%HasFastProperties stays true, while the map is not the old map.
Fast shape or dictionary mode?
  • A constructor always assigns name, then score, then active.
  • A hot row object deletes city after creation.
  • Object.freeze is called on a small settings object that already has all fields.
  • A plugin object receives 80 different property names at runtime.
  • Two object literals use { x, y } in the same order.
  • A completed config object is frozen before it is shared.
Try it yourself
0 of 6 correct

Sort each scenario by the representation the lesson's V8 facts or teaching model suggest.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Shape deprecation and migration

A map can become stale when the field representation it promised is no longer good enough. In V8, changing one object's field from a small integer to a double can create a replacement map. Other objects still on the old map may migrate lazily the next time they are accessed.

Step through shape deprecation and migration
Step 0 of 9Ready
Your turn: follow the blue line

Watch a teaching model for shape deprecation: one wider write creates a replacement map, and an old object migrates on next use.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const b = { map: "M1:x=Smi", x: 2 };console.log(a.map === b.map); b.x = 1.5;b.map = "M2:x=Double";const deprecated = "M1:x=Smi";console.log(a.map === b.map); if (a.map === deprecated) {  a.x = Number(a.x);  a.map = "M2:x=Double";}console.log(a.map === b.map);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Node-only representation-change probeJavaScript
const a = { x: 1 };const b = { x: 2 };console.log("start same", %HaveSameMap(a, b)); b.x = 1.5;console.log("after double same", %HaveSameMap(a, b)); console.log("A BEFORE");%DebugPrint(a);console.log("access", a.x);console.log("A AFTER");%DebugPrint(a);console.log("after access same", %HaveSameMap(a, b));
Verified runtime fact

The test sees deprecated_map before a.x is accessed, then stable_map and migration_target after access. %HaveSameMap(a, b) changes from true, to false, back to true.

Practical habits for developers

Most code should not think about hidden classes all day. The useful habits are simple: initialize expected fields in the same order, avoid delete on measured hot objects, prefer null or undefined for “not present yet” when the shape should stay stable, and measure before changing clear code.

Initialize the complete shape up frontPop out in the code editor (opens in a new tab)JavaScript
function makeCard(name, city) {  return {    name,    city,    selected: false,    error: null,  };} const first = makeCard("Asha", "Pune");const second = makeCard("Ravi", "Kochi");second.error = "missing phone";first.error = null; console.log(Object.keys(first).join(","));console.log(Object.keys(second).join(","));
  • Use factories or constructors that create the same fields in the same order for many similar objects.
  • Set a field to null or undefined instead of deleting it when the object stays hot.
  • Keep object shapes boring at the edges of render loops, parsers, and data transforms you have actually profiled.
  • Link this layout lesson to inline caches, prototype internals, property descriptors, and freezing and sealing rather than re-learning those topics here.

Common misconceptions

  • “Hidden classes are JavaScript classes.” They are engine layout records, not language syntax.
  • “Same properties always means same shape.” Order, attributes, representations, prototypes, and transitions can matter.
  • “Delete is always optimized away if it removes the last field.” The V8 12.4 probe in this lesson did not show that; both deletes became slow properties.
  • “Freeze makes objects slow.” Freezing changes the map, but small frozen objects can still have fast properties.
  • “Engine internals should override readability.” Stable shapes are useful only when a measured hot path points there.
Similar ideas that are easy to mix up
IdeaMeansDo not confuse it with
JavaScript semanticsObjects have properties, prototypes, descriptors, and observable operations.Hidden classes are not specified by ECMAScript.
Hidden class / map / shapeAn engine layout record for property names, attributes, representations, and offsets.It is not class User {} and not visible to ordinary code.
Inline cacheA site-local cache that can use maps/shapes as guards.This lesson explains the layout that ICs guard, not IC state machines again.
Dictionary modeA flexible property representation for churn-heavy objects.It does not make your code wrong; it matters only on measured hot paths.

Practice exercises

Exercise 1 · Warm-upPredict the transition comparisons

Read the teaching model and type the two booleans printed by the final lines.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const transitions = new Map();
function nextMap(current, property) {
  const key = current + "->" + property;
  if (!transitions.has(key)) transitions.set(key, current + "." + property);
  return transitions.get(key);
}

function makeRecord(label) {
  return { label, map: "Root", slots: {} };
}

function store(record, property, value) {
  record.map = nextMap(record.map, property);
  record.slots[property] = value;
  return record;
}

const asha = makeRecord("Asha");
store(asha, "name", "Asha");
store(asha, "score", 10);
const ravi = makeRecord("Ravi");
store(ravi, "name", "Ravi");
store(ravi, "score", 20);
const meera = makeRecord("Meera");
store(meera, "score", 30);
store(meera, "name", "Meera");

console.log(asha.map === ravi.map);
console.log(asha.map === meera.map);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the property metadata structure

    What V8 structure lists properties and offsets for a map?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead the delete probe

      What does the lesson's V8 probe print for delete stays fast?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWrite a shape-friendly factory

        Run the factory starter. What key order does it print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function makeRow(name) {
          return { name, score: 0, active: true };
        }
        console.log(Object.keys(makeRow("Asha")).join(","));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeTrace a migration

          What three booleans does the migration model print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const a = { map: "M1:x=Smi", x: 1 };
          const b = { map: "M1:x=Smi", x: 2 };
          console.log(a.map === b.map);
          
          b.x = 1.5;
          b.map = "M2:x=Double";
          const deprecated = "M1:x=Smi";
          console.log(a.map === b.map);
          
          if (a.map === deprecated) {
            a.x = Number(a.x);
            a.map = "M2:x=Double";
          }
          console.log(a.map === b.map);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a real app

            Name one habit you would apply to a measured hot list or data-transform object.

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Answer by separating language behavior from V8 implementation facts, then trace the small code snippets in order.

              Hidden classes quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is a hidden class or map?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this key-order clue print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const a = { x: 1, y: 2 };
                const b = { y: 2, x: 1 };
                console.log(Object.keys(a).join("|"));
                console.log(Object.keys(b).join("|"));

                Choose an answer to see the explanation.

              3. Question 3 of 8Why can maps form a transition tree?

                Choose an answer to see the explanation.

              4. Question 4 of 8Which statement about descriptor arrays is accurate?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this freeze snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const settings = { theme: "dark" };
                Object.freeze(settings);
                console.log(Object.isFrozen(settings));
                console.log(settings.theme);

                Choose an answer to see the explanation.

              6. Question 6 of 8In the V8 12.4 probe, what happened after delete on a named property?

                Choose an answer to see the explanation.

              7. Question 7 of 8What does this migration teaching model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const a = { map: "M1:x=Smi", x: 1 };
                const b = { map: "M1:x=Smi", x: 2 };
                console.log(a.map === b.map);
                b.x = 1.5;
                b.map = "M2:x=Double";
                const deprecated = "M1:x=Smi";
                console.log(a.map === b.map);
                if (a.map === deprecated) a.map = "M2:x=Double";
                console.log(a.map === b.map);

                Choose an answer to see the explanation.

              8. Question 8 of 8Which production habit follows from hidden classes?

                Choose an answer to see the explanation.

              Key takeaways

              • Hidden classes, maps, shapes, and structures are engine layout records for dynamic JavaScript objects.
              • Adding properties creates transition chains; same names in different orders can land on different maps.
              • Descriptor arrays describe properties and offsets, and maps on a transition chain can share them.
              • Fast properties use fixed layouts; dictionary mode handles deletes and heavy property churn.
              • Slack tracking trims early unused in-object fields, while representation changes can deprecate and migrate maps.
              • Use consistent shapes naturally, avoid hot deletes, and measure before optimizing.

              Remember the one-liner.
              A hidden class is the engine's shared form template for objects that were built the same way.

              You came from Symbols inside the engine. Up next: Fast arrays & elements kinds, where array storage gets its own set of layouts and transitions.

              CompleteFrontend Clear concepts. Working examples.