Performance & Profiling
⚡ Performance & Profiling
Memory Leaks: Where They Actually Hide
In a garbage-collected language, a "leak" means something is still reachable long after it should be — usually because something is holding a reference it forgot to release:
Forgotten Listeners
Chapter 7's TypedEventBus.on() without a matching off() keeps every subscribed callback (and everything it closes over) alive forever.
Unbounded Caches
A Map used as a cache that never evicts entries grows without limit — every unique key ever seen stays in memory.
Closures Over Large Objects
A callback that closes over a big object (even if it only uses one small field) keeps the entire object alive as long as the callback exists.
Singleton Containers
Chapter 1's singleton lifetime is exactly right for a connection pool — but registering something request-scoped as a singleton by mistake leaks that request's data forever.
🔥 CPU Profiling
Don't guess where time is going — measure it. Node ships a built-in CPU profiler; Chrome DevTools reads the output as a flame graph:
Flame Graph
Width = time spent. The widest bars, not the deepest stack, tell you what's actually expensive.
Self Time vs Total Time
"Self time" is time in that function alone; "total time" includes everything it calls — a function can have huge total time but tiny self time.
Common Bottlenecks in a TypeScript/Node App
Node is single-threaded for JavaScript execution — anything synchronous and slow blocks every other request on the event loop, the same event loop covered in the Node.js Fundamentals course:
Blocking vs Non-Blocking
Blocks the Event Loop
Every other request waits until these finish.
Yields Back to the Event Loop
Other requests keep being handled while this runs.
Also worth checking: N+1 database queries (Chapter 6), JSON.stringify on very large objects, and synchronous file I/O (fs.readFileSync) on a hot request path.
🧮 Optimization Strategies
Typed Memoization
Caching a pure function's results is one of the highest-value, lowest-risk optimizations — and generics keep it reusable across any function shape:
A Generic Memoize Function
Args extends unknown[]
A tuple type parameter — memoize works for any function signature while keeping every argument's type intact.
Cache Growth = New Leak Risk
An unbounded memoization cache trades a CPU problem for a memory one — pair it with an LRU eviction policy for anything called with unbounded input.
💻 Coding Challenges
Challenge 1: Find and Fix a Listener Leak
Given a route handler that calls bus.on() on every request without ever calling bus.off(), rewrite it to unsubscribe once the response finishes (hint: the res object emits a "finish" event).
Goal: Practice matching every subscription with an unsubscription tied to a clear lifetime.
Challenge 2: Move Blocking Work Off the Event Loop
Given a route handler that calls a synchronous, CPU-heavy function directly, rewrite it to run that work in a worker_thread so it no longer blocks other requests.
Goal: Practice recognizing and fixing an event-loop-blocking bottleneck.
Challenge 3: Add Eviction to a Memoize Cache
Extend the memoize function above with a maximum cache size — once the limit is reached, evict the least-recently-used entry before adding a new one.
Goal: Practice trading a CPU optimization for a bounded, safe memory cost instead of an unbounded one.
It's tempting to memoize everything, cache everything, and rewrite the "obviously slow" loop — before ever running a profiler. Most of the time the actual bottleneck is somewhere unexpected (a synchronous crypto call, an N+1 query, a huge JSON.stringify), and the "obvious" hot path was fine all along. Profile first, then optimize the function that the flame graph actually points to.
🎯 What's Next
With the app fast and leak-free, the final chapter ships it: Deployment & CI/CD — containerization, automated testing in pipelines, canary deployments, and rollback strategies.