DEV Community

Cover image for Object vs Map in JavaScript — A Performance Cheat Sheet
Hoang Duy
Hoang Duy

Posted on

Object vs Map in JavaScript — A Performance Cheat Sheet

It's duythenights again! Great to have you back.

When should you use Object and when should you use Map? In this post, I'll break it down with a side-by-side comparison, benchmarks, gotchas, and practical rules — cheat sheet style.

Try the Interactive Demo — benchmark Object vs Map with real API data in your own browser.

Map vs Object

TL;DR

Object Map
Key types string or Symbol only Any value (object, function, number...)
Insertion order Not guaranteed (except integer keys) Guaranteed
Size Object.keys(obj).length map.size — O(1)
Iteration Object.keys() / for...in for...of / .forEach() — native
Prototype pollution Yes No
Serialization JSON.stringify() native Manual conversion needed
Best for Struct-like data, fixed shape Dictionary, dynamic keys, frequent add/delete

Creating & Basic Operations

Object

// Create
const obj = {};
const obj2 = { name: "Alice", age: 30 };
const obj3 = Object.create(null); // no prototype — safer as dictionary

// Set
obj["key"] = "value";
obj.key = "value";

// Get
const val = obj["key"];
const val2 = obj.key;

// Check
"key" in obj;              // true — but checks prototype chain too
obj.hasOwnProperty("key"); // true — own properties only

// Delete
delete obj["key"];

// Size
Object.keys(obj).length;
Enter fullscreen mode Exit fullscreen mode

Map

// Create
const map = new Map();
const map2 = new Map([["name", "Alice"], ["age", 30]]);

// Set
map.set("key", "value");
map.set(42, "number key");       // any type as key
map.set(document.body, "DOM");   // even objects

// Get
const val = map.get("key");

// Check
map.has("key"); // true — no prototype chain issue

// Delete
map.delete("key");

// Size
map.size; // O(1) — direct property
Enter fullscreen mode Exit fullscreen mode

Iteration

Object

const user = { name: "Alice", age: 30, role: "admin" };

// Keys only
Object.keys(user);    // ["name", "age", "role"]

// Values only
Object.values(user);  // ["Alice", 30, "admin"]

// Entries
Object.entries(user); // [["name","Alice"], ["age",30], ["role","admin"]]

// for...in (includes inherited — usually not what you want)
for (const key in user) {
  if (user.hasOwnProperty(key)) {
    console.log(key, user[key]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Map

const config = new Map([["theme", "dark"], ["lang", "en"], ["debug", false]]);

// Native iterable — no intermediate array created
for (const [key, value] of config) {
  console.log(key, value);
}

// forEach
config.forEach((value, key) => {
  console.log(key, value);
});

// Destructured iterators
config.keys();    // MapIterator {"theme", "lang", "debug"}
config.values();  // MapIterator {"dark", "en", false}
config.entries(); // MapIterator {["theme","dark"], ["lang","en"], ...}
Enter fullscreen mode Exit fullscreen mode

The Prototype Trap

const obj = {};

// Looks empty, but...
"toString" in obj;      // true — inherited from Object.prototype
"constructor" in obj;   // true
"hasOwnProperty" in obj; // true

// This can cause bugs:
const cache = {};
cache["constructor"] = "my value";
// Shadows Object.prototype.constructor — unexpected behavior

// Safe alternative:
const safeObj = Object.create(null); // no prototype chain
"toString" in safeObj; // false
Enter fullscreen mode Exit fullscreen mode
const map = new Map();

// Clean — no prototype pollution
map.has("toString");    // false
map.has("constructor"); // false
// Map keys live in a completely separate namespace
Enter fullscreen mode Exit fullscreen mode

Key Type Flexibility

// Object: keys are coerced to strings
const obj = {};
obj[1] = "one";
obj["1"] = "string one";
console.log(obj[1]); // "string one" — 1 was coerced to "1"

obj[true] = "bool";
console.log(obj["true"]); // "bool" — true was coerced to "true"

const key = { id: 1 };
obj[key] = "object key";
console.log(obj["[object Object]"]); // "object key" — objects become "[object Object]"
Enter fullscreen mode Exit fullscreen mode
// Map: keys keep their original type
const map = new Map();
map.set(1, "number one");
map.set("1", "string one");
console.log(map.get(1));   // "number one"
console.log(map.get("1")); // "string one" — different keys!

const objKey = { id: 1 };
map.set(objKey, "object key");
map.get(objKey); // "object key" — works with reference equality
map.size; // 3
Enter fullscreen mode Exit fullscreen mode

Serialization

// Object: native JSON support
const obj = { name: "Alice", age: 30 };
const json = JSON.stringify(obj); // '{"name":"Alice","age":30}'
const parsed = JSON.parse(json);  // { name: "Alice", age: 30 }
Enter fullscreen mode Exit fullscreen mode
// Map: requires manual conversion
const map = new Map([["name", "Alice"], ["age", 30]]);

// Map → JSON
const json = JSON.stringify(Object.fromEntries(map));

// JSON → Map
const restored = new Map(Object.entries(JSON.parse(json)));

// Or for non-string keys:
const json2 = JSON.stringify([...map]); // serialize as array of entries
const restored2 = new Map(JSON.parse(json2));
Enter fullscreen mode Exit fullscreen mode

Performance: When It Matters

Both are O(1) for get/set/delete in Big-O terms, but constant factors differ significantly depending on how many keys you have.

Small collections (< ~30 keys)

Object wins or ties. V8 uses Hidden Classes and Inline Caching — property access is nearly as fast as accessing a field in a C struct.

// V8 internally creates a "hidden class" for this shape:
const user = { name: "Alice", age: 30, role: "admin" };

// Subsequent access is extremely fast because V8 knows
// the exact memory offset of each property.
user.name; // direct offset lookup — no hash needed
Enter fullscreen mode Exit fullscreen mode

Large collections (1000+ dynamic keys)

Map wins. When an Object has too many dynamically-added keys, V8 switches to "dictionary mode" — a hash table that's less optimized than Map's purpose-built one.

const lookup = {};
for (let i = 0; i < 10000; i++) {
  lookup[`key_${i}`] = i; // V8 gives up on hidden classes → dictionary mode
}
// At this point, lookup["key_500"] is a hash table access
// Map would be faster here because its hash table has
// less overhead (no prototype chain, no property descriptors)
Enter fullscreen mode Exit fullscreen mode

Frequent add/delete

Map wins clearly. delete obj[key] is one of the slowest operations in V8 because it breaks the hidden class chain and forces the engine to rebuild internal structures.

// This is expensive — don't do this in hot paths:
const cache = {};
cache["session_123"] = data;
delete cache["session_123"]; // breaks hidden class

// This is fast — Map is designed for this:
const cache = new Map();
cache.set("session_123", data);
cache.delete("session_123"); // O(1), no side effects
Enter fullscreen mode Exit fullscreen mode

Benchmark summary (1,000 entries)

Operation Winner Why
Insert Map Designed for dynamic additions
Lookup Map Object falls into dictionary mode at this size
Update Tie Both are simple hash table operations
Delete Map delete on Object breaks hidden classes
Has Key Map map.has() vs in (prototype chain traversal)
Iterate Map Native iterator vs Object.keys() array allocation

Try it yourself: Interactive Demo — benchmark Object vs Map with real API data in your own browser.


Decision Flowchart

Do you need JSON serialization?
├── Yes → Object
└── No
    └── Are keys always strings?
        ├── No → Map (only Map supports non-string keys)
        └── Yes
            └── Fixed shape, known at write time?
                ├── Yes → Object (V8 optimizes this heavily)
                └── No
                    └── Frequent add/delete?
                        ├── Yes → Map
                        └── How many keys?
                            ├── < 100 → Object (simpler, good enough)
                            └── 100+ → Map
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Use Object when:

  • Representing a record/struct with fixed fields (user.name, user.age)
  • Response/payload objects from APIs (JSON-native)
  • You need destructuring (const { name, age } = user)
  • Writing config/options objects passed to functions
  • Keys are always strings and the set is small and static

Use Map when:

  • Building a cache, lookup table, or registry
  • Keys are not strings (DOM nodes, objects, numbers)
  • You need guaranteed insertion order
  • You add and remove entries frequently
  • You need .size without counting
  • You want to iterate without Object.keys() overhead
  • You want no prototype pollution risk

Common Patterns

Pattern: Object as struct, Map as index

interface User {
  id: number;
  name: string;
  email: string;
}

// Object for data shape (struct)
const user: User = { id: 1, name: "Alice", email: "alice@dev.to" };

// Map for lookup index (dictionary)
const usersById = new Map<number, User>();
usersById.set(user.id, user);

// Fast lookup
const found = usersById.get(1); // O(1), type-safe key
Enter fullscreen mode Exit fullscreen mode

Pattern: Map for counting

function wordFrequency(text: string): Map<string, number> {
  const freq = new Map<string, number>();
  for (const word of text.split(/\s+/)) {
    freq.set(word, (freq.get(word) ?? 0) + 1);
  }
  return freq; // iteration order = insertion order
}
Enter fullscreen mode Exit fullscreen mode

Pattern: Map for caching with object keys

const cache = new Map<HTMLElement, DOMRect>();

function getCachedRect(el: HTMLElement): DOMRect {
  if (cache.has(el)) return cache.get(el)!;
  const rect = el.getBoundingClientRect();
  cache.set(el, rect); // DOM element as key — impossible with Object
  return rect;
}
Enter fullscreen mode Exit fullscreen mode

Pattern: WeakMap for private data (no memory leak)

const metadata = new WeakMap<object, { createdAt: number }>();

function track(obj: object) {
  metadata.set(obj, { createdAt: Date.now() });
}

// When obj is garbage collected, its WeakMap entry is automatically removed
// This is impossible with Object or Map
Enter fullscreen mode Exit fullscreen mode

If you found this useful, drop a reaction. Questions? Let's discuss in the comments.

Top comments (0)