DEV Community

Cover image for The Hidden Cost of Object Spread
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

The Hidden Cost of Object Spread

One of the most common pieces of JavaScript you'll find in modern codebases looks like this:

const nextState = {
  ...state,
  count: state.count + 1
}
Enter fullscreen mode Exit fullscreen mode

React developers write it.

Redux developers write it.

Vue developers write it.

Angular developers write it.

Nearly every frontend framework encourages some variation of it.

It feels clean.

It feels immutable.

It feels modern.

It feels almost free.

But here's the uncomfortable truth:

Object spread is often far more expensive than most developers realize.

This doesn't mean object spread is bad.

It doesn't mean immutability is bad.

It doesn't mean you should stop using it.

But it does mean you should understand what it's actually doing.

Because once you understand the cost, you start making much better engineering decisions.


The Illusion

When developers see:

const nextState = {
  ...state,
  count: state.count + 1
}
Enter fullscreen mode Exit fullscreen mode

they often mentally model it as:

Take state
Change count
Done
Enter fullscreen mode Exit fullscreen mode

But that's not what happens.

JavaScript doesn't magically update the object.

Instead it performs something conceptually closer to:

const nextState = {}

for (const key in state) {
  nextState[key] = state[key]
}

nextState.count =
  state.count + 1
Enter fullscreen mode Exit fullscreen mode

Notice the difference.

We're not updating.

We're copying.

Every property.

Every time.


What Actually Happens

Consider:

const user = {
  id: 1,
  name: "John",
  email: "john@example.com"
}

const updatedUser = {
  ...user,
  active: true
}
Enter fullscreen mode Exit fullscreen mode

Internally:

Allocate New Object
↓
Copy id
↓
Copy name
↓
Copy email
↓
Add active
Enter fullscreen mode Exit fullscreen mode

For three properties?

Nobody cares.

For thousands of properties?

You probably should.


The Cost Grows With Size

Imagine:

const hugeObject = {
  ...
}
Enter fullscreen mode Exit fullscreen mode

containing:

10,000 properties
Enter fullscreen mode Exit fullscreen mode

Now:

{
  ...hugeObject,
  updated: true
}
Enter fullscreen mode Exit fullscreen mode

must:

Allocate New Object
+
Copy 10,000 Properties
+
Add One Property
Enter fullscreen mode Exit fullscreen mode

Just to change one value.

That's not free.


The Famous Reduce Trap

This is one of the most common performance issues I see.

const usersById =
  users.reduce(
    (acc, user) => ({
      ...acc,
      [user.id]: user
    }),
    {}
  )
Enter fullscreen mode Exit fullscreen mode

Looks elegant.

Looks immutable.

Looks functional.

Looks expensive.

Let's see why.


Iteration 1

{}
Enter fullscreen mode Exit fullscreen mode

Copy:

0 properties
Enter fullscreen mode Exit fullscreen mode

Iteration 2

{
  1: user1
}
Enter fullscreen mode Exit fullscreen mode

Copy:

1 property
Enter fullscreen mode Exit fullscreen mode

Iteration 3

{
  1: user1,
  2: user2
}
Enter fullscreen mode Exit fullscreen mode

Copy:

2 properties
Enter fullscreen mode Exit fullscreen mode

Iteration 1000

Copy 999 properties
Enter fullscreen mode Exit fullscreen mode

Total Work

What initially looks like:

O(n)
Enter fullscreen mode Exit fullscreen mode

can become closer to:

O(n²)
Enter fullscreen mode Exit fullscreen mode

because every iteration copies everything accumulated so far.

That is a very different performance profile.


The Loop Equivalent

Compare:

const usersById =
  users.reduce(
    (acc, user) => ({
      ...acc,
      [user.id]: user
    }),
    {}
  )
Enter fullscreen mode Exit fullscreen mode

with:

const usersById = {}

for (const user of users) {
  usersById[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode

The loop:

Creates One Object
Mutates One Object
Performs One Pass
Enter fullscreen mode Exit fullscreen mode

No repeated copies.

No repeated allocations.

No repeated garbage collection.


Why React Popularized Object Spread

This is where things get interesting.

React didn't make object spread popular by accident.

React relies heavily on:

Reference Equality
Enter fullscreen mode Exit fullscreen mode

Example:

if (
  previousState !== nextState
) {
  rerender()
}
Enter fullscreen mode Exit fullscreen mode

This works beautifully when:

const nextState = {
  ...state,
  count: 1
}
Enter fullscreen mode Exit fullscreen mode

because:

New Object
New Reference
Enter fullscreen mode Exit fullscreen mode

React can instantly detect the change.

This is a great reason to use object spread.

But:

Useful
≠
Free
Enter fullscreen mode Exit fullscreen mode

Deeply Nested Objects

This is where things get ugly.

Suppose:

const nextState = {
  ...state,
  user: {
    ...state.user,
    address: {
      ...state.user.address,
      city: "Dubai"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You've probably written something similar.

Maybe many times.

Let's count.

Copy state
Copy user
Copy address
Update city
Enter fullscreen mode Exit fullscreen mode

Multiple allocations.

Multiple copies.

For one change.


Why Immer Became Popular

This problem became so common that libraries emerged specifically to solve it.

One of the most popular is Immer.

Instead of:

const nextState = {
  ...state,
  user: {
    ...state.user,
    address: {
      ...state.user.address,
      city: "Dubai"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Immer allows:

draft.user.address.city =
  "Dubai"
Enter fullscreen mode Exit fullscreen mode

while still producing an immutable result.

This dramatically improves readability.


Garbage Collection Matters

Most developers focus on CPU.

But memory matters too.

Every spread operation creates:

Temporary Objects
Enter fullscreen mode Exit fullscreen mode

Those objects eventually become:

Garbage
Enter fullscreen mode Exit fullscreen mode

Which means:

Garbage Collection
Enter fullscreen mode Exit fullscreen mode

must clean them.

The larger the application becomes:

More Allocations
↓
More Garbage
↓
More GC Work
Enter fullscreen mode Exit fullscreen mode

Sometimes the bottleneck isn't computation.

It's memory churn.


The Hidden Cost In State Management

Consider:

return {
  ...state,
  loading: true
}
Enter fullscreen mode Exit fullscreen mode

Looks harmless.

Now imagine:

100 updates per second
Enter fullscreen mode Exit fullscreen mode

across:

Multiple Stores
Multiple Components
Large State Trees
Enter fullscreen mode Exit fullscreen mode

Suddenly those allocations become measurable.

Not catastrophic.

Just measurable.

And that's the point.


When Object Spread Is Perfect

Let's be fair.

Object spread solves real problems.

Example:

const updatedUser = {
  ...user,
  active: true
}
Enter fullscreen mode Exit fullscreen mode

Clear.

Readable.

Predictable.

For small objects:

Use it.

Without hesitation.


When You Should Be Careful

Pay attention when you see:

Large Collections
Enter fullscreen mode Exit fullscreen mode

or

Large State Trees
Enter fullscreen mode Exit fullscreen mode

or

Reducers Executing Frequently
Enter fullscreen mode Exit fullscreen mode

or

Performance-Critical Loops
Enter fullscreen mode Exit fullscreen mode

This is where spread begins to matter.


Benchmark Mentality

One of the biggest mistakes developers make is assuming:

Spread Is Slow
Enter fullscreen mode Exit fullscreen mode

or

Spread Is Fast
Enter fullscreen mode Exit fullscreen mode

Both are wrong.

The correct answer is:

It Depends
Enter fullscreen mode Exit fullscreen mode

How many properties?

How often?

How frequently is the code executed?

How large are the objects?

Engineering is always contextual.


Real World Example: API Processing

Bad:

const result =
  users.reduce(
    (acc, user) => ({
      ...acc,
      [user.id]: user
    }),
    {}
  )
Enter fullscreen mode Exit fullscreen mode

Better:

const result = {}

for (const user of users) {
  result[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode

The second version scales significantly better.


Real World Example: React State

Good:

setUser({
  ...user,
  name: "John"
})
Enter fullscreen mode Exit fullscreen mode

The object is small.

Readability wins.

Optimization would be pointless.


Real World Example: Deep Updates

Instead of:

{
  ...state,
  a: {
    ...state.a,
    b: {
      ...state.a.b,
      c: value
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

consider:

  • State normalization
  • Immer
  • Better state structure

Architecture often beats optimization.


Pros Of Object Spread

1. Readable

Intent is obvious.


2. Immutable

Reduces accidental mutations.


3. React-Friendly

Works perfectly with reference equality.


4. Predictable

Creates explicit state transitions.


5. Easy To Learn

Minimal cognitive overhead.


Cons Of Object Spread

1. Allocations

Every spread creates a new object.


2. Property Copying

The larger the object, the more expensive the copy.


3. Garbage Collection Pressure

Temporary objects accumulate.


4. Easy To Abuse In Reducers

Repeated spreads can become surprisingly expensive.


5. Deep Updates Become Ugly

Nested spreads quickly reduce readability.


The Real Lesson

The biggest mistake developers make with object spread is treating it as a free operation.

It isn't.

The second biggest mistake is treating it as a bad operation.

It isn't.

Object spread is a tool.

A very useful tool.

A very readable tool.

A very common tool.

But every immutable update has a cost.

The goal isn't avoiding object spread.

The goal is understanding when its benefits outweigh its costs.

Because once you understand the tradeoff, you stop blindly copying objects.

And start making deliberate engineering decisions.


What's Next?

In the next article we'll discuss:

Composability Is The Real Superpower

Because after exploring:

  • Reduce
  • Transducers
  • Functors
  • FlatMap
  • Monads
  • RxJS
  • Event Sourcing

you'll discover that all of them ultimately revolve around a single idea:

Composition
Enter fullscreen mode Exit fullscreen mode

And that idea is far more important than any individual function.


About The Author

Hi, I'm Amrish Khan.

I enjoy building developer tools, exploring software architecture, and writing about the deeper ideas behind everyday programming concepts.

I'm also building Aruvix — a growing ecosystem of local-first developer tools designed to process data directly in the browser without unnecessary uploads.

Here's a detailed blog on Aruvix:

https://dev.to/amrishkhan05/aruvix-the-ultimate-offline-first-developer-toolkit-e0i

You can follow my work and thoughts here:

Portfolio:
https://www.amrishkhan.dev

LinkedIn:
https://www.linkedin.com/in/amrishkhan

GitHub:
https://www.github.com/amrishkhan05

If you enjoyed this article, consider following for more deep dives into JavaScript, architecture, local-first software, and performance engineering.

Top comments (0)