loke.dev
Header image for Stop Freezing Every Object (Leverage V8 Hidden Classes for Faster Immutability Instead)

Stop Freezing Every Object (Leverage V8 Hidden Classes for Faster Immutability Instead)

Your quest for immutability might be the very thing slowing you down—uncover why Object.freeze can break V8's most critical optimizations.

· 4 min read

Stop Freezing Every Object (Leverage V8 Hidden Classes for Faster Immutability Instead)

I used to be a "freeze zealot." I’d look at a piece of state in a React component or a configuration object and think, *if I don't Object.freeze() this right now, some rogue junior developer (or, let's be honest, future me at 2 AM) is going to mutate it and break everything.* I thought I was being a performance hero by enforcing immutability.

The reality? I was actually handing the V8 engine a giant wrench and telling it to jam it into the gears.

While immutability is a fantastic architectural goal, Object.freeze() is often the most expensive way to achieve it. To understand why, we have to look under the hood at how JavaScript engines actually "see" your objects.

V8 and the Magic of Hidden Classes

JavaScript is a dynamic language, but the engines that run it (like V8 in Chrome and Node.js) crave stability. To speed things up, V8 uses something called Hidden Classes (sometimes called "Shapes").

When you create an object, V8 assigns it a hidden class. If you create another object with the exact same properties in the exact same order, they share that hidden class. This allows V8 to use "Inline Caching"—basically a shortcut to remember exactly where a property lives in memory so it doesn't have to do a slow dictionary lookup every time.

// This is fast. V8 creates one hidden class for 'User'.
function User(name, age) {
  this.name = name;
  this.age = age;
}

const user1 = new User('Alice', 30);
const user2 = new User('Bob', 25);

As long as user1 and user2 share that shape, V8 can optimize any code that touches them.

How Object.freeze Breaks the Magic

When you call Object.freeze(obj), you aren't just making the properties read-only. You are fundamentally changing the "integrity level" of the object.

V8 can no longer assume the object will behave like its peers. In many cases, freezing an object forces V8 to move it into Dictionary Mode (also known as "Slow Mode"). The object stops being a sleek, optimized structure and becomes a literal hash map. Every time you access a property, the engine has to do a real-world search through the map instead of jumping straight to the memory offset.

const config = { host: 'localhost', port: 8080 };

// Everything is fine... until:
Object.freeze(config); 

// V8: "Okay, I can't optimize this based on its shape anymore. 
// It's now a special snowflake. Moving to Dictionary Mode."

In high-frequency code paths—like a Redux reducer or a game loop—this performance tax adds up fast.

The "Consistent Shape" Alternative

If you want the benefits of immutability without the performance hit of Object.freeze, the secret is consistent initialization.

Instead of creating an empty object and slowly hydrating it (which creates a chain of different hidden classes), create your objects in one shot with all the fields they will ever have.

The Bad Way (Slow Shapes)

const point = {};
point.x = 10; // Shape A
point.y = 20; // Shape B

The Better Way (One Shape)

const point = { x: 10, y: 20 }; // Shape A (and it stays that way)

By ensuring your objects always have the same keys in the same order, you’re already doing 90% of the work V8 needs to keep your code screaming fast.

Real Immutability Without the Freeze

"But wait," you say, "I still need to make sure nobody changes my object!"

If you're using a modern build toolchain (and you probably are), TypeScript is your best friend here. Use readonly or Readonly<T> to enforce immutability at compile-time. This gives you the developer experience of "I can't touch this" without the runtime overhead of the engine having to guard the object.

interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
}

const config: AppConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

// TypeScript will yell at you. 
// V8 will still see a perfectly optimized, "fast" object.
config.apiUrl = 'modified'; // Error: Cannot assign to 'apiUrl' because it is a read-only property.

When Should You Actually Use Object.freeze?

I'm not saying Object.freeze is evil. It’s a tool. It’s perfectly fine for:

1. Small, one-off configuration objects that are accessed rarely.
2. Security boundaries where you absolutely cannot trust the code receiving the object (e.g., passing an object to a third-party script).
3. Debugging to find exactly where a pesky mutation is happening.

But if you’re iterating over an array of 10,000 objects in a data-processing pipeline, please, for the love of your users' CPU, don't freeze them.

The Performance Result

If you benchmark a simple property access on a "normal" object vs. a "frozen" object in a tight loop, the results are eye-opening. The optimized hidden class access can be up to 10x faster than the dictionary lookup required by a frozen or "polluted" object.

The takeaway: Trust your types for safety and keep your object shapes consistent for speed. Let V8 do what it does best: optimize predictable code. Stop making it work so hard to protect you from yourself.