โฑ๏ธ Reading time: 6-8 minutes
๐ฏ Difficulty: Intermediate
๐ Themes: Memory Management, JavaScript Internals, Angular, Performance
What's inside?
- ๐ The Problem: My journey toward becoming a senior developer โ and the question that started it all.
- ๐ฆ Stack & Heap: Where data actually lives in RAM โ and why it matters.
- ๐ Value vs Reference Types: Why copying objects is more dangerous than you think.
- ๐ const and Object.freeze: What they actually protect โ and what they don't.
- ๐งน Garbage Collection: Where memory leaks really come from.
- โก Change Detection: How Angular uses everything above to decide when to re-render.
๐ The Problem
I've been working as a frontend developer for quite some time.
Self-taught, no CS degree โ just building real Angular
applications in production.
At some point I made a decision: I want to become a senior
developer. Not just someone who makes things work, but someone
who understands why they work.
So I started going deep. My first target was Angular Change
Detection โ I knew Angular compared values to decide when to
re-render. But when I looked closer, a simple question stopped me:
Why does this return false?
[] === [] // false
And why does this return true?
"10" === "10" // true
I knew the answers from experience. But I couldn't explain them
at a fundamental level โ and that gap bothered me.
That question pulled me deeper. Change Detection led me to
references. References led me to memory. And memory led me to
two concepts I should have learned a long time ago:
Stack and Heap.
This is the first post in my Road to Senior series โ
documenting everything I'm learning on the way to becoming
a senior developer. Starting where I probably should have
started from the beginning.
๐ฆ Stack & Heap โ Where Your Data Actually Lives
Most JavaScript developers know variables store data.
But very few know where that data physically lives in memory.
The answer is: it depends on what type of data it is.
Your computer's RAM is divided into two distinct regions:
the Stack and the Heap. They exist for different
purposes, behave differently, and understanding the difference
will change how you think about JavaScript forever.
The Stack
Think of the Stack like a pile of trays in a cafeteria.
You add trays to the top, you take trays from the top.
Last in, first out โ always.
The Stack is:
- Fast โ adding and removing data is just moving a pointer
- Small โ typically 1-8 MB
- Automatic โ data is cleaned up the moment a function ends
- Fixed-size โ every slot must have a known, predictable size
When you call a function, JavaScript creates a new "frame"
on the Stack for that function's local variables.
When the function returns, the frame is gone.
function greet() {
let name = "Jan" // stored directly on the Stack
let age = 25 // stored directly on the Stack
}
// function ends โ both variables are gone from Stack
This works perfectly for numbers, booleans, and strings โ
types with a fixed, predictable size.
The Heap
The Heap is different. Think of it as a large,
unorganized warehouse. When you need space for something,
the system finds a free spot, stores it there,
and gives you the address of where it put it.
The Heap is:
- Large โ can grow to gigabytes
- Flexible โ can store data of unknown or dynamic size
- Managed โ cleaned up by the Garbage Collector (more on this later)
- Slower โ finding free space takes more work than moving a pointer
Objects live here. Because an object can have 2 properties
or 200 โ JavaScript doesn't know the size in advance.
const user = { name: "Jan", role: "admin" }
// โ address stored on Stack
// โ actual object stored on Heap
The Key Insight
When you create an object, two things happen:
- The actual object is created somewhere in the Heap
- A reference (memory address) to that object is stored on the Stack
const user = { name: "Jan" }
// Stack Heap
// โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
// user โ 0x4A2F โ { name: "Jan" }
// โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
The variable user doesn't hold the object.
It holds the address of where the object lives.
This is why they're called reference types โ
and it's exactly what makes copying objects so dangerous.
Which brings us to the next topic.
๐ Value vs Reference Types
Now that you know about Stack and Heap, this next part
will make complete sense.
JavaScript has two categories of data types โ and they
behave completely differently when you copy them.
Value Types
These seven types store their value directly on the Stack:
-
numberโ42,3.14,-7 -
stringโ"hello",'Jan' -
booleanโtrue,false nullundefinedsymbolbigint
When you copy a value type, JavaScript copies the actual value.
The two variables are completely independent.
let a = 42
let b = a // copies the VALUE โ b gets its own slot on the Stack
b = 99
console.log(a) // 42 โ untouched
console.log(b) // 99
Changing b has zero effect on a. They live in separate
memory slots on the Stack.
Reference Types
Everything else is a reference type:
-
objectโ{},{ name: 'Jan' } -
arrayโ[],[1, 2, 3] -
functionโ() => {} -
Map,Set - class instances โ
new User()
When you copy a reference type, JavaScript copies the address โ
not the object itself. Both variables point to the
same object in the Heap.
const a = { name: 'Jan' }
const b = a // copies the ADDRESS โ both point to the same object
b.name = 'Eva'
console.log(a.name) // 'Eva' โ original changed!
console.log(b.name) // 'Eva'
There is only one object in the Heap. Two variables, one address.
Changing it through b changes it for a as well โ
because they're the same thing.
The Fix โ Create a New Reference
If you want a truly independent copy, you need to create
a new object in the Heap with its own address.
The spread operator does exactly that:
const a = { name: 'Jan' }
const updated = { ...a, name: 'Eva' } // new object in Heap
console.log(a.name) // 'Jan' โ untouched
console.log(updated.name) // 'Eva'
Two different addresses. Two independent objects.
How === Actually Works
This is where most developers get surprised.
For value types, === compares the actual values:
"10" === "10" // true โ same value
42 === 42 // true โ same value
For reference types, === compares the addresses โ
not the contents:
[] === [] // false โ two different addresses in Heap
{} === {} // false โ two different addresses in Heap
Even though the contents look identical, each [] and {}
creates a brand new object at a new address in the Heap.
Different addresses โ false.
But this returns true:
const a = { name: 'Jan' }
const b = a
a === b // true โ same address
The rule:
===on reference types is not asking
"do these objects look the same?"
It's asking "are these the exact same object in memory?"
Now you understand references. But what does const actually
protect โ and what doesn't it protect? That's next.
๐ const and Object.freeze
By now you know that variables store either a value
(on the Stack) or an address (on the Stack, pointing to the Heap).
const and Object.freeze both sound like they "lock" things.
But they lock very different things โ and confusing them
is one of the most common mistakes in JavaScript.
What const Actually Does
const locks the Stack slot. That's it.
It prevents you from reassigning the variable โ
meaning you can't make it point to a different address.
const user = { name: 'Jan' }
user = { name: 'Eva' } // โ TypeError โ trying to change the Stack slot
But the object in the Heap? Completely unprotected.
const user = { name: 'Jan' }
user.name = 'Eva' // โ
works โ we're changing the Heap, not the Stack slot
Stack Heap
โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
user โ 0x4A2F ๐ { name: 'Eva' } โ unprotected, freely mutable
โโโโโโโโโโโโโ
const does not mean immutable. It means
the reference cannot be reassigned.
What Object.freeze Does
Object.freeze locks the Heap content.
It prevents adding, removing, or changing properties on the object.
const user = Object.freeze({ name: 'Jan' })
user.name = 'Eva' // โ silently fails (or TypeError in strict mode)
user.age = 30 // โ silently fails
Now the Heap content is protected.
Stack Heap
โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
user โ 0x4A2F ๐ { name: 'Jan' } ๐
โโโโโโโโโโโโโ
Both the Stack slot and the Heap content are locked.
This is the closest JavaScript gets to a truly immutable object.
The Catch โ freeze is Shallow
Object.freeze only protects the first level.
Nested objects are still mutable.
const user = Object.freeze({
name: 'Jan',
address: { city: 'Prague' } // nested object โ NOT frozen
})
user.name = 'Eva' // โ blocked
user.address.city = 'Brno' // โ
works โ nested object is unprotected
To freeze deeply, you'd need to recursively freeze
every nested object โ known as a "deep freeze."
JavaScript doesn't have this built in.
Quick Summary
| Protects Stack slot | Protects Heap content | |
|---|---|---|
const |
โ | โ |
Object.freeze |
โ | โ (shallow only) |
const + Object.freeze
|
โ | โ (shallow only) |
If
constmeant truly immutable,
Object.freezewouldn't need to exist.
Now you understand how references work and how to protect them.
But what happens to objects when nothing references them anymore?
That's where memory leaks come from.
๐งน Garbage Collection โ Where Memory Leaks Really Come From
In C or C++, when you allocate memory, you are responsible
for freeing it. Forget to free it โ memory leak.
Free it twice โ crash.
JavaScript handles this automatically. That's the job of
the Garbage Collector (GC).
But "automatic" doesn't mean "bulletproof."
Understanding how GC works is exactly what separates
developers who write memory-safe code from those who don't.
The Fundamental Rule
As long as a reference to an object exists,
the GC will not delete it.
An object without any reference pointing to it is considered
"dead" โ unreachable. The GC is free to delete it and
reclaim that memory.
This one rule explains every memory leak you'll ever encounter.
How It Works โ Mark-and-Sweep
The algorithm V8 uses is called mark-and-sweep.
It runs in two phases:
Phase 1 โ Mark
The GC starts from the "roots" โ the Stack, global variables,
and currently executing functions. It follows every reference
and marks every reachable object.
Phase 2 โ Sweep
Everything that wasn't marked is considered unreachable.
The GC deletes it and frees the memory.
Stack (roots) Heap
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ
user โ 0x01 โ โ
{ name: 'Jan', addr: 0x02 }
โ
{ city: 'Prague' } โ reachable via 0x01
โ { name: 'old' } โ nothing points here โ deleted
โโโโโโโโโโโโโโ
Notice: the object at 0x02 is kept even though the Stack
doesn't point to it directly โ it's reachable through 0x01.
The GC follows the entire chain of references.
When Does GC Run?
You can't control it. You can't call it manually.
V8 decides when to run it based on memory pressure.
V8 uses two types of GC:
- Minor GC โ fast, runs frequently, cleans up short-lived objects
- Major GC โ slow, runs rarely, cleans the entire Heap. Can briefly pause the JS thread โ which is why avoiding unnecessary object creation matters in performance-critical code.
Memory Leaks โ When GC Can't Help You
A memory leak happens when you hold a reference to an object
you no longer need. The GC sees the reference and thinks
you still need it. It can't delete it.
The most common source of memory leaks in Angular?
Forgotten subscriptions.
// โ leak โ subscription stays alive after component is destroyed
ngOnInit() {
this.service.data$.subscribe(d => {
this.data = d
})
}
// component gets destroyed
// but the subscription still holds a reference to this component
// GC cannot clean it up
Every time a user navigates to this component and away,
another subscription is created. None of them are cleaned up.
The app slowly gets slower.
The fix:
// โ
option 1 โ manual unsubscribe
ngOnDestroy() {
this.subscription.unsubscribe()
}
// โ
option 2 โ modern Angular
data = toSignal(this.service.data$) // auto-unsubscribes on destroy
// โ
option 3 โ also modern Angular
this.service.data$
.pipe(takeUntilDestroyed())
.subscribe(d => this.data = d)
Why Angular Cares So Much About This
Angular applications are SPAs โ the page never fully reloads.
Components are created and destroyed dynamically as users navigate.
In a traditional website, navigating away reloads the page
and clears everything from memory. In an SPA, nothing clears
unless you explicitly clean it up.
A forgotten subscription creates a reference chain:
subscription โ Observable โ data โ component
The GC sees references at every step.
Nothing gets cleaned. Memory grows. App slows down.
This is why toSignal(), takeUntilDestroyed(), and DestroyRef
exist โ they're Angular's built-in answer to this problem.
They remove the reference automatically when the component is destroyed,
giving the GC what it needs to do its job.
Now you understand Stack, Heap, references, and GC.
It's time to see how Angular uses all of this to decide
when to update your UI.
โก Change Detection โ How Angular Uses Everything Above
This is why all of the above matters.
Everything you've learned so far โ Stack, Heap, references,
=== comparison โ Angular's Change Detection is built
on exactly these concepts.
The Core Question
Angular's job is to keep the UI in sync with your data.
But re-rendering every component on every possible change
would be incredibly expensive.
So Angular asks a simpler question:
Did the reference change?
oldRef === newRef // true โ same address โ skip
oldRef === newRef // false โ new address โ re-render
That's it. Angular does a === comparison on references.
The same === you now understand at a memory level.
Why Mutation Doesn't Work
When you mutate an object, the address on the Stack
stays the same. Angular does ===, gets true,
and skips the component.
// โ Angular won't detect this change
this.user.name = 'Eva'
// The Stack still holds the same address โ === is true โ no re-render
When you create a new object with spread,
a new address is created in the Heap.
Angular does ===, gets false, and re-renders.
// โ
Angular detects this change
this.user = { ...this.user, name: 'Eva' }
// New object in Heap โ new address on Stack โ === is false โ re-render
This is not Angular magic. This is just === on memory addresses.
What I Know Now
When I started this journey, I had a simple question:
why does [] === [] return false?
The answer turned out to be a complete mental model:
-
[]creates a new object in the Heap - The variable stores the address on the Stack
-
===compares addresses, not content - Angular uses
===to detect changes - Mutating an object keeps the same address โ no re-render
- Creating a new object creates a new address โ re-render
- Keeping unnecessary references prevents GC from cleaning up โ memory leak
This is the foundation. NgRx immutability, OnPush, Signals โ
all of it builds on top of what you just learned.
But that's a story for another post.
Top comments (0)