text
$ top -pid 49201
Processes: 612 total, 2 running, 610 sleeping, 3214 threads
Load Avg: 2.41, 2.10, 1.95 CPU usage: 14.22% user, 8.11% sys, 77.66% idle
SharedLibs: 412M resident, 51M data, 62M linkedit.
MemRegions: 182934 total, 4102M resident, 112M private, 1204M shared.
PhysMem: 16G used (3210M wired), 214M unused.
PID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS STATE
49201 node 12.4 00:41.02 11 1 24 1.2G 0B 412M sleeping
Look at that. Look at the terminal output above. That is a Node.js process running a "modern" microservice. It’s a glorified JSON transformer. It has three endpoints. It doesn't do Fourier transforms. It doesn't render 3D geometry. It doesn't manage a real-time database. It sits there, bloated and lethargic, sucking down 1.2 gigabytes of RAM like a dying star collapsing under its own gravity.
Listen, kid, I don't care that you went to a six-week bootcamp and think you're a "Full Stack Engineer" because you can copy-paste a React component. If you're asking me **what is javascript** while your Node process is currently eating 90% of the production server's swap space, then we have a fundamental problem with your education. You think you’re writing code. You’re actually just piling garbage on top of a landfill and hoping the smell doesn't reach the users.
JavaScript isn't a programming language. It’s a 10-day mistake that the world decided to turn into a global standard because we were too lazy to build something better. It is a case study in architectural regret, a monument to the "good enough" philosophy that has effectively set computing back twenty years.
## The Original Sin of Brendan Eich
In 1995, Brendan Eich was told he had ten days to create a scripting language for Netscape Navigator. Ten days. I’ve had bowel movements that lasted longer than the design phase of the world’s most popular runtime. The goal wasn't to create a robust, performant, or even logical language. The goal was to win a marketing war against Microsoft.
The name itself is a lie. It has nothing to do with Java. It was named "JavaScript" to piggyback on the hype of Sun Microsystems' language, a desperate attempt to make a toy look like a tool. Because of that ten-day sprint, we are stuck with architectural decisions that haunt every CPU cycle executed in the 21st century.
Eich had to borrow bits and pieces from Scheme, Self, and Java, then stitch them together like a digital Frankenstein. He gave us a language with no integer type—everything is a double-precision 64-bit binary format IEEE 754 floating point. Do you understand how insane that is? If you want to count from one to ten, the CPU has to handle it as a floating-point operation unless the JIT engine is smart enough to optimize it away.
The "Original Sin" wasn't just the rush; it was the decision to make the language "forgiving." In C, if you do something stupid, the compiler screams at you, or the program segfaults. In JavaScript, the language tries to guess what you meant. It’s like a waiter who, when you order a steak, brings you a live cow and a blowtorch because "maybe you wanted to do it yourself."
## The Illusion of Type Safety
Let’s talk about the "truthy" and "falsy" logic that makes my blood pressure spike every time I see a pull request. In a sane language, `true` is `true` and `false` is `false`. In JavaScript, the number `0` is false, but the string `"0"` is true. An empty array `[]` is true, but `[] == false` evaluates to `true`.
Explain that to a CPU. Explain the branching logic required to handle the `Abstract Equality Comparison Algorithm` defined in the ECMAScript spec. When you write `x == y`, the engine has to go through a 14-step decision tree just to figure out if you're comparing a string to a number or an object to a boolean.
```javascript
// The Hall of Shame
console.log([] == ![]); // true
console.log(NaN === NaN); // false
console.log(typeof null); // "object" (a bug from 1995 they can't fix because it would "break the web")
console.log(0.1 + 0.2 === 0.3); // false
The CPU hates you for this. Modern processors are marvels of branch prediction and speculative execution. They want to know what’s coming next. But when the types of your variables can change at any millisecond because you decided to reassign a string to a variable that previously held an integer, the CPU’s pipeline stalls. It flushes the cache. It waits. It sighs.
We’ve tried to fix this with TypeScript, which is essentially putting a tuxedo on a pig. TypeScript doesn’t exist at runtime. It’s a security blanket for developers that disappears the moment the code actually runs. You spend all day defining interfaces and types, only for the transpiler to strip them away and leave you with the same chaotic, type-coercing garbage that was there before. It’s a layer of bureaucracy that provides the illusion of safety while the underlying engine is still running on a foundation of sand.
Table of Contents
V8: Putting a Turbocharger on a Lawnmower
Google realized around 2008 that the web was getting slow because JavaScript was fundamentally broken. Their solution wasn’t to replace the language—that would be too logical. Instead, they built V8, a massive, multi-million line C++ engine designed to JIT-compile this mess into machine code.
V8 is an engineering miracle, but it’s a miracle built to solve a problem that shouldn’t exist. It uses “Hidden Classes” (or “Shapes”) to try and bring some order to the chaos. When you create an object in JavaScript, it doesn’t have a fixed layout in memory like a C struct. You can add properties to it whenever you feel like it.
To handle this, V8 creates a hidden class. If you add a property x, it creates a new hidden class. If you add y, it creates another one. But if you create another object and add y then x, V8 sees that as a different hidden class. Now your “Inline Caching” is blown. The engine can’t optimize the property access because the “shape” of the object is unstable.
// A glimpse into the madness: node --trace-gc
[49201:0x118008000] 23 ms: Scavenge 12.4 (15.2) -> 10.1 (16.2) MB, 0.8 / 0.0 ms (average idle time 0.0 ms, TLB flush 0.0 ms) [allocation failure].
[49201:0x118008000] 45 ms: Mark-sweep 15.2 (20.2) -> 12.1 (22.2) MB, 4.2 / 0.0 ms (average idle time 0.0 ms, TLB flush 0.0 ms) [GC in old space requested].
Look at those GC logs. “Scavenge.” “Mark-sweep.” That’s the Garbage Collector desperately trying to find memory to free up because you created 50,000 short-lived objects in a single loop. In C, I know exactly when my memory is allocated and freed. In JavaScript, the “Stop-the-world” events happen whenever the engine feels like it. Your UI stutters, your server latency spikes, and your CPU burns cycles just to figure out if that variable from three functions ago is still being used.
V8 has two compilers: Ignition (the interpreter) and TurboFan (the optimizing compiler). If a function is called enough times, it gets “hot,” and TurboFan tries to turn it into optimized machine code. But if the types change—even once—TurboFan “deoptimizes” the code and throws it back to the interpreter. We are literally spending CPU cycles to compile code, then spending more cycles to throw that code away when the language’s inherent instability rears its head.
The Event Loop: A Single-Threaded Prison
Then we have the Event Loop. Some genius decided that the best way to handle I/O was to make everything single-threaded and asynchronous. “Non-blocking I/O!” they shouted from the rooftops, while their applications crawled to a halt.
The Event Loop is a lie. It’s a single thread running in a circle, checking a queue. If you do anything—anything at all—that requires actual computation, you block the entire world. If you calculate a Fibonacci sequence on the main thread, your web server stops responding to requests. Your “non-blocking” server is now a “very-much-blocking” paperweight.
To “fix” this, we got the callback hell of the early 2010s. Then we got Promises. Then we got async/await. Each one is just a more expensive layer of syntactic sugar designed to hide the fact that we’re trying to do concurrency on a single track.
Every time you use await, you’re creating a new Promise object, which goes onto the heap, which eventually needs to be garbage collected. You’re adding overhead to every single function call. We’ve traded the simplicity of threads and locks for a convoluted system of microtasks and macrotasks that even senior developers don’t fully understand.
Try explaining the difference between setImmediate, process.nextTick, and setTimeout(0) to someone who actually understands how a kernel schedules tasks. They’ll laugh in your face. It’s a Rube Goldberg machine of scheduling, all because we’re terrified of actual multi-threading in the language.
Node.js and the Colonization of the Server
In 2009, Ryan Dahl took V8 out of the browser and put it on the server. He has since apologized for this, but the damage is done. Node.js is the ultimate expression of architectural hubris. We took a runtime designed to display dancing hamsters and decided it was the perfect foundation for our financial systems and enterprise infrastructure.
Node.js v21.6.2 is a behemoth. It brings the entire V8 engine, the libuv abstraction layer, and a massive standard library written in a mix of JS and C++. When you start a Node process, you’re not just running your script; you’re booting up a massive, memory-hungry virtual environment.
And the memory leaks. Oh, the memory leaks. In Express.js, it’s remarkably easy to accidentally keep a reference to a request object in a closure. Suddenly, every request your server handles stays in memory forever. Because the GC is non-deterministic, you don’t notice the leak until the process hits the --max-old-space-size limit and crashes with an OOM (Out of Memory) error.
<--- Last few GCs --->
[49201:0x110008000] 15203 ms: Mark-sweep 1192.3 (1224.1) -> 1191.8 (1224.1) MB, 450.2 / 0.0 ms (average idle time 0.0 ms, TLB flush 0.0 ms) [last resort gc].
[49201:0x110008000] 15653 ms: Mark-sweep 1191.8 (1224.1) -> 1191.8 (1224.1) MB, 450.0 / 0.0 ms (average idle time 0.0 ms, TLB flush 0.0 ms) [last resort gc].
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
1: 0x1012e35a0 node::Abort() (.cold.1) [/usr/local/bin/node]
2: 0x1000a58b9 node::Abort() [/usr/local/bin/node]
3: 0x1000a5a1f node::OnFatalError(char const*, char const*) [/usr/local/bin/node]
4: 0x1001e7807 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
There it is. The “Last Resort GC.” The engine is screaming, trying to find even a single byte of free space, and failing. This is what modern “scalability” looks like: spinning up 50 Docker containers, each running a 1.2GB Node process, just to handle a load that a single well-written C++ or Rust binary could handle on a Raspberry Pi.
The Dependency Black Hole
Finally, we must address the node_modules folder—the heaviest object in the known universe. Because JavaScript’s standard library was historically non-existent, the community decided that every single basic function should be its own third-party package.
You want to pad a string? There’s a package for that. You want to check if a number is even? There’s a package for that. This led to the left-pad incident, where one developer deleted a few lines of code and broke half the internet.
When you run npm install, you aren’t just downloading code. You’re downloading a recursive tree of dependencies that often reaches hundreds of levels deep. You end up with five different versions of the same utility library because five different packages in your tree require them.
$ npm install
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR! react@"18.2.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.8.0 || ^17.0.0" from [email protected]
npm ERR! node_modules/some-garbage-library
npm ERR! some-garbage-library@"*" from the root project
This is your life now. Fighting version conflicts in a dependency tree that is too large for any human to audit. You have 500MB of code in node_modules just to center a div and send a POST request. Most of that code will never be executed, but it all has to be parsed, and in the case of build tools like Webpack or Vite, it all has to be bundled and minified.
We’ve reached a point where we need a “compiler” (Babel) to turn modern JavaScript into old JavaScript, a “bundler” (Webpack) to stitch thousands of files together, and a “minifier” (Terser) to make the resulting mess small enough to send over a wire. We are using more energy to build and serve a “Hello World” website today than it took to run the entire infrastructure of a mid-sized city in 1970.
The ECMAScript 2023 spec keeps adding features—decorators, pipeline operators, new array methods like toSorted() and toReversed(). They’re just rearranging the deck chairs on the Titanic. The core of the language is still the same rushed, type-less, single-threaded mess it was in 1995.
Every time you launch an Electron app—whether it’s Slack, Discord, or VS Code—you are launching a full instance of the Chromium browser just to display a chat window. You are dedicating gigabytes of RAM to something that should take megabytes. We are wasting the greatest hardware ever built on the most inefficient software ever conceived.
The heat death of the universe is being accelerated by every npm install. Every time a V8 JIT-compiler has to re-optimize a function because you passed a null instead of an undefined, a fraction of a joule is wasted. Multiply that by the billions of devices running this garbage, and you realize that JavaScript isn’t just a bad language—it’s an ecological disaster.
Go back to your desk. Fix that memory leak. And for the love of the CPU, stop using ==. Not that it matters. The damage is already done. We’re all just living in Brendan Eich’s ten-day fever dream, waiting for the OOM killer to finally put us out of our misery.
Related Articles
Explore more insights and best practices: