JavaScript Map, Set, WeakMap & WeakSet — The Ultimate Performance Guide
If you're still using objects for everything and arrays for lookups, you're writing slow JavaScript. The difference between O(1) and O(n) performance isn't academic - it's the difference between an app that feels buttery smooth and one that chugs under load.
In 2026, Map, Set, WeakMap, and WeakSet aren't just ES6 curiosities. They're fundamental tools that senior developers reach for daily to build performant, memory-efficient applications. Let's dive deep into when and how to use each.
The Problem: Why Objects and Arrays Aren't Always Enough
JavaScript objects have been the go-to key-value store for decades. But they come with surprising limitations:
// Objects work great for string keys...
const user = { name: "Alice", age: 30 };
// But fail for non-string keys
const map = new Map();
map.set(document.getElementById("myButton"), "button data"); // ✅ Works
map.set(123, "numeric data"); // ✅ Works
const obj = {};
obj[document.getElementById("myButton")] = "button data"; // ❌ Coerced to "[object Object]"
obj[123] = "numeric data"; // ✅ Becomes obj["123"]Objects also don't preserve insertion order (until recently), can't easily track size, and make performance optimization guesswork.
Map: When Order and Performance Matter
A Map is a collection of key-value pairs where keys can be any value (including objects, numbers, or even symbols).
Basic Usage
const userMap = new Map();
// Add entries
userMap.set("id", 123);
userMap.set("name", "Bob");
userMap.set("active", true);
// Get values
console.log(userMap.get("name")); // "Bob"
console.log(userMap.has("age")); // false
console.log(userMap.size); // 3Key Advantages Over Objects
1. Any Key Type
const map = new Map();
// Object keys (impossible with plain objects)
const button1 = document.createElement("button");
const button2 = document.createElement("button");
map.set(button1, "First button");
map.set(button2, "Second button");
// Symbol keys
const mySymbol = Symbol("description");
map.set(mySymbol, "Symbol value");
// Number keys (stays as number, not stringified)
map.set(42, "Answer to life");2. Guaranteed Insertion Order
const map = new Map();
map.set("z", "last");
map.set("a", "first");
map.set("b", "second");
// Always insertion order: ["z", "a", "b"]
for (const [key, value] of map) {
console.log(key, value);
}Performance: Map vs Object Lookup
// Test setup
const data = {};
const map = new Map();
const iterations = 100000;
// Populate data
for (let i = 0; i < iterations; i++) {
const key = `key_${i}`;
const value = `value_${i}`;
data[key] = value;
map.set(key, value);
}
// Benchmark object lookup
console.time("Object lookup");
for (let i = 0; i < iterations; i++) {
const key = `key_${i}`;
const value = data[key]; // O(1) average
}
console.timeEnd("Object lookup");
// Benchmark map lookup
console.time("Map lookup");
for (let i = 0; i < iterations; i++) {
const key = `key_${i}`;
const value = map.get(key); // O(1)
}
console.timeEnd("Map lookup");Result: Maps consistently outperform objects for dynamic key sets, especially when keys are added or removed frequently.
Real-World: Caching API Responses
class APICache {
constructor() {
this.cache = new Map();
this.maxSize = 100;
}
async fetch(key, fetchFunction) {
// Check cache first
if (this.cache.has(key)) {
return this.cache.get(key);
}
// Fetch from API
const data = await fetchFunction();
// Add to cache
this.cache.set(key, data);
// Clean up if cache is full (FIFO style)
if (this.cache.size > this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
return data;
}
}
// Usage
const apiCache = new APICache();
const userData = apiCache.fetch("user_123", () => fetch("/api/users/123"));Set: When You Need Unique Values Fast
A Set is a collection of unique values. It's essentially an optimized version of an array where uniqueness is guaranteed.
Basic Usage
const uniqueUsers = new Set();
// Add values (duplicates are ignored)
uniqueUsers.add("Alice");
uniqueUsers.add("Bob");
uniqueUsers.add("Alice"); // Ignored
uniqueUsers.add("Charlie");
console.log(uniqueUsers.size); // 3
console.log(uniqueUsers.has("Bob")); // trueCommon Patterns
1. Array Deduplication
const users = ["Alice", "Bob", "Alice", "Charlie", "Bob"];
const uniqueUsers = [...new Set(users)];
// ["Alice", "Bob", "Charlie"]2. Fast Membership Testing
const allowedRoles = new Set(["admin", "editor", "viewer"]);
function hasPermission(userRole, requiredRole) {
return allowedRoles.has(requiredRole);
}
// O(1) lookup vs O(n) with array
console.log(hasPermission("editor", "editor")); // true
console.log(hasPermission("user", "admin")); // false3. Counting Unique Values
function countUnique(arr) {
return new Set(arr).size;
}
const scores = [85, 92, 85, 78, 92, 85];
console.log(countUnique(scores)); // 4 (unique: 85, 92, 78)Performance: Set vs Array.includes()
// Test setup
const arr = Array.from({length: 100000}, (_, i) => `item_${i}`);
const set = new Set(arr);
// Benchmark array search
console.time("Array.includes()");
for (let i = 0; i < 1000; i++) {
arr.includes(`item_${Math.floor(Math.random() * 100000)}`);
}
console.timeEnd("Array.includes()");
// Benchmark set search
console.time("Set.has()");
for (let i = 0; i < 1000; i++) {
set.has(`item_${Math.floor(Math.random() * 100000)}`);
}
console.timeEnd("Set.has()");Result: Set.has() is consistently 100-1000x faster than Array.includes() for large arrays.
WeakMap: Memory-Efficient Object Associations
This is where it gets interesting. A WeakMap is like a Map, but it doesn't prevent its keys from being garbage collected. This makes it perfect for associating metadata with objects without memory leaks.
The Problem WeakMap Solves
// ❌ MEMORY LEAK: Object keys create strong references
const data = new Map();
const user = { id: 123, name: "Alice" };
data.set(user, "Some sensitive data");
// Even if user is deleted from elsewhere...
// user = null;
// ...the data in our Map keeps the object alive
// The Map entry will persist until we explicitly delete it
data.delete(user);WeakMap to the Rescue
// ✅ MEMORY SAFE: Keys can be garbage collected
const metadata = new WeakMap();
const user = { id: 123, name: "Alice" };
// Associate metadata without keeping object alive
metadata.set(user, {
lastLogin: new Date(),
preferences: { theme: "dark" }
});
// When user object is no longer referenced elsewhere,
// it can be garbage collected, and the WeakMap entry disappears automaticallyReal-World: DOM Element Metadata
class DOMManager {
constructor() {
this.elementData = new WeakMap();
}
setElementData(element, data) {
this.elementData.set(element, data);
}
getElementData(element) {
return this.elementData.get(element);
}
// No cleanup needed! Elements are automatically
// removed when they're garbage collected
}Performance: Perfect Event Listeners
// ❌ MEMORY LEAK: All listeners stay active
function setupClickHandlers() {
const buttons = document.querySelectorAll("button");
buttons.forEach(button => {
button.addEventListener("click", handleClick);
});
}
// ✅ MEMORY EFFICIENT: Listeners cleaned up automatically
function setupClickHandlers() {
const buttons = document.querySelectorAll("button");
const clickHandlers = new WeakMap();
buttons.forEach(button => {
const handler = (e) => {
console.log(`Button ${button.textContent} clicked`);
};
clickHandlers.set(button, handler);
button.addEventListener("click", handler);
// When button is removed from DOM, handler
// can be garbage collected automatically
});
}WeakSet: Weak References for Object Collections
A WeakSet is to Set what WeakMap is to Map - it holds weak references to objects, allowing them to be garbage collected.
Usage Examples
// Track visited DOM elements (no memory leaks)
const visitedElements = new WeakSet();
function markElementVisited(element) {
visitedElements.add(element);
}
function isElementVisited(element) {
return visitedElements.has(element);
}
// Usage
const button = document.querySelector("button");
markElementVisited(button);
if (isElementVisited(button)) {
console.log("Button has been visited");
}Use Cases for WeakSet
1. Object State Tracking
const activeInstances = new WeakSet();
class DatabaseConnection {
constructor(url) {
this.url = url;
activeInstances.add(this);
}
close() {
// Cleanup
activeInstances.delete(this);
}
}
// When instances are no longer referenced,
// they can be garbage collectedPerformance Comparison: When to Use What
| Operation | Object | Array | Map | Set |
|---|---|---|---|---|
| Add key/value | ✅ Fast | ❌ Slow (push) | ✅ Fast (set) | ✅ Fast (add) |
| Lookup by key | ✅ O(1) avg | ❌ O(n) | ✅ O(1) | N/A |
| Lookup by value | ❌ O(n) | ❌ O(n) | ❌ O(n) | ✅ O(1) |
| Delete key | ✅ Fast | ❌ O(n) | ✅ Fast | ✅ Fast |
| Size | ❌ Manual tracking | ✅ .length | ✅ .size | ✅ .size |
| Key types | Strings only | N/A | Any value | N/A |
| Garbage collection | ❌ Strong refs | ❌ Strong refs | ✅ Weak refs (WeakMap) | ✅ Weak refs (WeakSet) |
Interview Questions: Test Your Knowledge
Q1: What's the difference between Object.keys() and Map.keys()?
const obj = { a: 1, b: 2 };
const map = new Map([['a', 1], ['b', 2]]);
// Object.keys() returns string array in insertion order (ES6+)
Object.keys(obj); // ["a", "b"]
// Map.keys() returns iterator (more memory efficient)
map.keys(); // MapIterator {"a", "b"}Q2: When would you use Map instead of Object for caching?
// Use Map when:
// - Keys aren't strings (objects, symbols, numbers)
// - Need guaranteed insertion order
// - Need to know size without counting properties
// - Need frequent additions/removals
const cache = new Map();
cache.set({ id: 123 }, "data"); // ✅ Object as key
cache.set(42, "answer"); // ✅ Number as keyQ3: How does WeakMap help prevent memory leaks? WeakMap keys are weak references. When the key object is no longer referenced anywhere else in your code, it can be garbage collected, and the WeakMap entry is automatically removed. This prevents memory leaks when you're associating metadata with objects that might be removed from the DOM or otherwise destroyed.
Best Practices
1. Choose the Right Data Structure
// ❌ Using object for dynamic string keys
const cache = {};
const data = fetch("api/endpoint");
cache[`user_${userId}`] = data;
// ✅ Using Map for any key types
const cache = new Map();
const data = fetch("api/endpoint");
cache.set(`user_${userId}`, data);2. Clear Memory When Done
// ❌ Potential memory leak
function processLargeDataset() {
const data = loadData();
const results = new Map();
// Process data...
return results;
}
// ✅ Clear when done
function processLargeDataset() {
const data = loadData();
const results = new Map();
// Process data...
results.clear(); // Free memory
return results;
}3. Use WeakMap for DOM Associations
// ❌ Creates strong references
const elementData = new Map();
// ✅ Allows garbage collection
const elementData = new WeakMap();Performance Optimization: When Order Matters
// Track processing order
const processingQueue = new Map();
function addToQueue(task) {
processingQueue.set(task.id, {
task,
timestamp: Date.now(),
status: "pending"
});
}
function processInOrder() {
// Process in insertion order
for (const [id, taskData] of processingQueue) {
if (taskData.status === "pending") {
processTask(taskData.task);
taskData.status = "completed";
}
}
}Conclusion: From Junior to Senior
Mastering these data structures separates developers who write functional code from those who write efficient, scalable applications:
- Map: When you need O(1) lookups with any key type and guaranteed order
- Set: When you need unique values and fast membership testing
- WeakMap: When associating metadata with objects without memory leaks
- WeakSet: When tracking object collections without keeping them alive
The next time you reach for an object or array, ask yourself: "Could a Map or Set make this faster, more memory-efficient, or easier to reason about?" The answer is probably yes.
Takeaways
- Use Map when you need key-value pairs with non-string keys or guaranteed order
- Use Set for unique values and O(1) membership testing
- Use WeakMap to associate metadata with objects without memory leaks
- Use WeakSet to track object collections without preventing garbage collection
- Performance matters: O(1) lookups beat O(n) searches every time
Now go rewrite that slow object-based cache and watch your application performance soar.