DEV Community

Cover image for Why Functional Code Can Be Slower
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

Why Functional Code Can Be Slower

Functional Programming has a marketing problem.

Or perhaps more accurately:

Functional Programming has a reality problem.

If you spend enough time reading articles about FP, you'll eventually encounter claims like:

More elegant

More composable

More predictable

More maintainable
Enter fullscreen mode Exit fullscreen mode

And honestly?

Many of those claims are true.

I use functional patterns regularly.

I enjoy them.

I write about them.

This entire article series has been exploring concepts like:

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

But there is a topic that rarely gets discussed honestly:

Functional code can be slower.

Sometimes dramatically slower.

And understanding why makes you a better engineer.

Because performance doesn't care how elegant your abstractions are.


The Myth

Many developers unconsciously assume:

More Functional
=
More Modern
=
More Efficient
Enter fullscreen mode Exit fullscreen mode

This is not true.

Consider:

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

Looks elegant.

Looks immutable.

Looks functional.

But it can also be incredibly expensive.

Why?

Because every iteration creates a brand-new object.


What Actually Happens

Suppose:

{
  a: 1,
  b: 2
}
Enter fullscreen mode Exit fullscreen mode

becomes:

{
  ...obj,
  c: 3
}
Enter fullscreen mode Exit fullscreen mode

JavaScript doesn't magically update the object.

It creates:

New Object
+
Copy Existing Properties
+
Add New Property
Enter fullscreen mode Exit fullscreen mode

Every time.

Now imagine:

10000
Enter fullscreen mode Exit fullscreen mode

iterations.

You aren't just doing:

10000 writes
Enter fullscreen mode Exit fullscreen mode

You're doing:

Thousands of object copies
Enter fullscreen mode Exit fullscreen mode

which is a completely different cost.


The Loop Version

Compare:

const usersById = {}

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

This mutates one object.

No copies.

No allocations.

No repeated spreads.

From a performance perspective:

The loop wins.
Enter fullscreen mode Exit fullscreen mode

Almost every time.


Functional Doesn't Mean Free

Consider:

const result = users
  .map(transformA)
  .map(transformB)
  .map(transformC)
Enter fullscreen mode Exit fullscreen mode

Looks clean.

But internally:

Array
↓
Array
↓
Array
↓
Array
Enter fullscreen mode Exit fullscreen mode

Each map creates a new collection.

For:

100
Enter fullscreen mode Exit fullscreen mode

items?

Nobody cares.

For:

1000000
Enter fullscreen mode Exit fullscreen mode

items?

You probably should.


The Hidden Cost Of Immutability

One of the biggest performance tradeoffs in FP is immutability.

Example:

return {
  ...state,
  count: state.count + 1
}
Enter fullscreen mode Exit fullscreen mode

versus:

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

The immutable version:

  • Allocates memory
  • Copies properties
  • Creates garbage

The mutable version:

  • Updates in place

Much cheaper.


Garbage Collection Is Not Free

Most performance discussions stop at CPU.

But memory matters too.

Every allocation creates work for:

Garbage Collector
Enter fullscreen mode Exit fullscreen mode

Consider:

array
  .map(...)
  .filter(...)
  .map(...)
  .flatMap(...)
Enter fullscreen mode Exit fullscreen mode

Each stage may create:

Temporary Objects
Temporary Arrays
Temporary Closures
Enter fullscreen mode Exit fullscreen mode

Eventually:

GC Pause
Enter fullscreen mode Exit fullscreen mode

must clean them.


Closures Have A Cost

Every callback:

x => x * 2
Enter fullscreen mode Exit fullscreen mode

creates machinery.

Modern engines optimize aggressively.

But optimization is not magic.

Compare:

for (let i = 0; i < len; i++) {
  result[i] = data[i] * 2
}
Enter fullscreen mode Exit fullscreen mode

with:

data.map(
  x => x * 2
)
Enter fullscreen mode Exit fullscreen mode

The difference is often small.

But it exists.

And at scale:

Small differences accumulate.


Why Transducers Exist

Remember our Transducers article?

This problem is exactly why Transducers were invented.

Without Transducers:

data
  .filter(...)
  .map(...)
  .filter(...)
  .map(...)
Enter fullscreen mode Exit fullscreen mode

Multiple passes.

Multiple arrays.

Multiple allocations.

With Transducers:

Single Pass
Single Reduction
No Intermediate Arrays
Enter fullscreen mode Exit fullscreen mode

Same functionality.

Much better memory behavior.


Why RxJS Often Feels Fast

This surprises people.

RxJS can sometimes outperform traditional collection processing.

Why?

Because:

One Value
At A Time
Enter fullscreen mode Exit fullscreen mode

Instead of:

Entire Collection
Enter fullscreen mode Exit fullscreen mode

The pipeline processes:

Value
↓
Transform
↓
Next Value
Enter fullscreen mode Exit fullscreen mode

Memory remains stable.


The V8 Factor

Modern JavaScript engines are incredibly smart.

But they have expectations.

For example:

{
  id: 1,
  name: "John"
}
Enter fullscreen mode Exit fullscreen mode

and:

{
  name: "John",
  id: 1
}
Enter fullscreen mode Exit fullscreen mode

may not be treated identically internally.

Hidden classes.

Inline caches.

Object shapes.

All influence performance.

Repeated object spreading can interfere with these optimizations.


Performance Is A Tradeoff

The mistake many developers make is assuming:

Readable
vs
Fast
Enter fullscreen mode Exit fullscreen mode

is a binary choice.

It isn't.

Most systems spend their lives here:

Readable Enough
Fast Enough
Enter fullscreen mode Exit fullscreen mode

That's the sweet spot.


A Practical Rule

For:

Small datasets
Normal APIs
Typical UI code
Enter fullscreen mode Exit fullscreen mode

Choose clarity.

Always.

Nobody wins awards for micro-optimizing:

50
Enter fullscreen mode Exit fullscreen mode

items.


When Performance Starts Mattering

Pay attention when:

Large datasets

Real-time processing

Streaming systems

Analytics

Data pipelines

Rendering loops
Enter fullscreen mode Exit fullscreen mode

appear.

Now those abstractions become measurable.


The Most Expensive Functional Pattern

This pattern:

array.reduce(
  (acc, item) => ({
    ...acc,
    [item.id]: item
  }),
  {}
)
Enter fullscreen mode Exit fullscreen mode

is probably responsible for more accidental JavaScript slowdowns than any other FP pattern.

It looks elegant.

It benchmarks terribly.


The Most Important Performance Lesson

Many developers ask:

Which is faster?

reduce()
or

for...of
Enter fullscreen mode Exit fullscreen mode

Wrong question.

The real question is:

What work is being done?
Enter fullscreen mode Exit fullscreen mode

Because:

reduce()
Enter fullscreen mode Exit fullscreen mode

with mutation:

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

is very different from:

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

Same API.

Very different performance characteristics.


Pros Of Functional Code

1. Easier Composition

Functions combine naturally.


2. Easier Testing

Pure functions are predictable.


3. Better Abstractions

Patterns become reusable.


4. Less Shared Mutable State

Fewer side effects.


5. Better Reasoning

State transitions become explicit.


Cons Of Functional Code

1. More Allocations

Especially with immutable updates.


2. More Garbage Collection

Temporary objects accumulate.


3. Callback Overhead

Functions are not free.


4. Intermediate Collections

Repeated map/filter chains create extra work.


5. Can Hide Performance Problems

Elegant code often disguises expensive operations.


The Real Lesson

The biggest mistake developers make is turning programming paradigms into religions.

Functional Programming isn't better.

Object-Oriented Programming isn't better.

Procedural Programming isn't better.

They're tools.

And every tool has tradeoffs.

Functional patterns give us:

Composability

Predictability

Maintainability
Enter fullscreen mode Exit fullscreen mode

But sometimes they cost:

Memory

CPU

Allocations
Enter fullscreen mode Exit fullscreen mode

The best engineers understand both sides.

They know when to reach for:

map()
reduce()
flatMap()
Enter fullscreen mode Exit fullscreen mode

And they know when a simple:

for...of
Enter fullscreen mode Exit fullscreen mode

is exactly the right solution.

Because ultimately:

The goal isn't writing the most functional code.

The goal is writing the right code.


What's Next?

In the next article we'll discuss:

When a for...of Loop Is Better Than reduce()

Because after spending multiple articles exploring the power of reduce(), it's time to answer a question many developers quietly wonder:

Should I even be using reduce here?
Enter fullscreen mode Exit fullscreen mode

And surprisingly often, the answer is:

No.
Enter fullscreen mode Exit fullscreen mode

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)