Vivian Voss

The Unauditable Ones

javascript node architecture performance

Open a fresh terminal. Pick the most common starting point for a modern web project, the one a bootcamp will tell you to take this week: npm create vite@latest my-app -- --template react-ts. Three minutes later, npm install settles. The shell returns. The project has not yet run.

What is on disk:

  • Roughly 250 to 400 MB in node_modules for the leaner choice, Vite + React + TypeScript
  • A typical Next.js install reaches 700 MB to a full gigabyte, the majority devDependencies
  • Around 100,000 to 220,000 files
  • 30 to 50 packages listed in package.json
  • 1,200 to 1,800 packages actually pulled in, the transitive closure
  • A maintainer count in the hundreds, often four-figure

You have not written a line. You have already accepted the work of a small town's worth of strangers, each free to publish a new version overnight, and each free, in the npm trust model, to do almost anything on the next install.

What an Ordinary npm create vite@latest Buys You package.json 30 – 50 packages what you wrote transitive closure 1,200 – 1,800 what they brought maintainers hundreds — four-figure strangers, free to publish on disk (Vite + React + TS) 250 – 400 MB the leaner choice on disk (Next.js) 700 MB – 1 GB majority devDependencies file count: 100,000 – 220,000 the project has not yet run This is the ordinary case. Not the edge case. The starting point.

This is the ordinary case. Not the edge case. The starting point.

The point of this Bow is not that one thing in this picture is broken. The point is that four things broke at the same time, each independently against the lean principle that the engineer's head should be free for the work, and each, on its own, would deserve its own diagnosis. Together they have produced a software stack that is, in the precise sense, unauditable: nobody can tell you, with a defensible line, what runs on the next install.

Four Things Broke at Once 1 the count was never justified 1,200 – 1,800 packages 700 MB — a gigabyte on disk none required by the project the alternatives run at scale today 2 the boundary was never drawn no responsibility line post-install can modify others latest tag can shift retroactively libc has a line; node_modules has none 3 the workaround became architecture Node single-threaded, 2009 containers are the cores image: 150 MB → 1 GB Prime Video, Segment, Istio walk back 4 layers hide what they should bound ORM hides SQL mesh hides services operator hides YAML, kubelet, container containing became postponing

One: The Count Was Never Justified

A modern web project arrives with one to two thousand packages, several hundred megabytes of disk, and a hundred thousand files. None of this is required by what the project actually does. HTMX delivers reactivity to a browser in fourteen kilobytes minified, around five gzipped. MediaWiki runs sixty million Wikipedia articles on a server-rendered PHP stack with no SPA. GOV.UK is mandated to progressive enhancement and ships HTML-first. Hacker News is rendered server-side in Common Lisp. Hotwire, in production at Basecamp, gives full reactivity over the wire. Cloudflare replaced the standard nginx+Lua web tier with a Rust service, Pingora, and runs around seventy per cent less CPU at one trillion requests per day.

The 700 megabytes are not a consequence of what the application needs. They are a choice. The alternatives are not theoretical; they are running, today, at scale.

The Alternatives Run at Scale Today Next.js install ~ 700 MB to a full gigabyte majority devDeps before you write a line running in production HTMX ~ 14 KB min, 5 KB gz MediaWiki 60M Wikipedia articles, PHP GOV.UK progressive enhancement, HTML-first Hotwire (Basecamp) reactivity over the wire Cloudflare Pingora (Rust) ~70% less CPU at 1 trillion req/day, replacing nginx + Lua Hacker News — server-side, Common Lisp the 700 MB are a choice, not a requirement

Two: The Boundary Was Never Drawn

A node_modules directory has no responsibility line. Not because the code is open source — so is libc, and nobody reads libc either. The line is absent because the architecture never drew one.

There is no boundary between your application code and the 1,500 packages it pulls in. They run with the same permissions. They can read each other. The post-install script of any one of them can modify the others. The latest tag can shift retroactively, as the axios maintainer found on the morning of 31 March 2026. The maintainer of a popular package can be socially engineered, or simply hand over the keys.

libc, by contrast, has a line: it lives in the base source tree of an operating system, maintained by named engineers in a foundation with a public roadmap, released on a regular cadence, with reproducible builds and signed artifacts. You do not read it. You know where it lives, who is accountable for it, and how it arrived on your disk. node_modules has none of these.

Where the Line Is Drawn libc (base OS) named engineers foundation, public roadmap regular release cadence reproducible builds signed artifacts base source tree you do not read it but you know where it lives node_modules no responsibility line shared permissions post-install can modify others latest tag shifts retroactively maintainer can be engineered 1,500 packages, one heap you do not read it and you cannot say where it lives

Three: The Workaround Became Architecture

Node.js arrived in 2009 with a single-threaded event loop. Multi-core hardware required multi-container deployment, because the runtime could not use the cores. The fix was more containers and an orchestrator to schedule them.

Two decades later, the container is the unit of thought, not the deployment artefact: a minimal Node.js image is 150 MB, the convenient default a full gigabyte, and dockerd at 183 containers has been documented above five gigabytes of memory. The container architecture is not the right answer to isolation. It is a workaround for a language that cannot use the machine, dressed up as architecture.

Amazon Prime Video returned its video monitoring to a monolith, a ninety per cent cost reduction. Segment consolidated 140 services into one. Istio merged its control plane back into a single binary. The reversal is happening. The original architecture was sold by advertisement, not by parts removed.

How a Single-Threaded Runtime Became Container Architecture Node.js (2009) single-threaded loop multi-core hardware runtime cannot use it one container per Node the workaround orchestrator to schedule them the container is now the unit of thought 150 MB minimal image • ~1 GB convenient default • dockerd at 183 containers >5 GB RAM the reversal is happening Prime Video monolith (−90% cost) • Segment 140 → 1 • Istio control plane → single binary

Four: The Layers Hide What They Should Bound

The ORM hides the SQL. The cache hides the ORM. The service mesh hides the services. The operator hides the YAML, which hides the kubelet, which hides the container, which hides the process. Each layer was, in its origin, an idea about containing complexity, so that an engineer could reason one layer at a time.

Somewhere between Dijkstra's 1968 THE Operating System paper and the modern stack, the verb shifted: containing became postponing. Layers no longer keep their lower neighbour from leaking; they exist to defer the moment of examining it. Lehman's second law of software evolution, written in 1974, said it plainly: complexity rises unless explicit work is done to reduce it. The reduction is the work nobody is funded to do.

A Stack of Hiders ORM hides ↓ SQL / cache hides ↓ service mesh hides ↓ operator / YAML hides ↓ kubelet hides ↓ container hides ↓ the process Dijkstra 1968: containing complexity. Lehman 1974: it rises unless reduced.

These are four diagnoses. They share a system. None of them, on its own, is the whole problem.

Eighteen Wednesdays, One Investigation

Each of the four breaks deserves its own week. None can be honestly handled in a paragraph. The next four Wednesdays take the axes one at a time — the count, the boundary, the workaround, the layers.

Then three pillars, across the rest of the summer and into the autumn. npm in detail (15 July – 12 August): the registry that wasn't a service but a market; the code quality question with names attached; the maintainer economy that the system asks for and does not pay for; the audit industry that sells the cure for the disease the registry sells as a feature; the alternatives that did not win, and why. Docker in detail (19 August – 16 September): image bloat as a default; the container escape register; the orchestrator stack; the resource cost; the firms walking back, in their own words. Teams and the labour market (23 – 30 September): how sprint-shaped organisations produce service-shaped architecture; how the hiring filter locks both in.

And the closing Wednesday, 7 October, asks what could still be.

Eighteen Wednesdays, One Investigation Phase 1 The Four Axes 17 Jun – 8 Jul count boundary workaround layers 4 weeks Phase 2 npm in Detail 15 Jul – 12 Aug the registry quality + names audit industry alternatives 5 weeks Phase 3 Docker in Detail 19 Aug – 16 Sep image bloat escape register orchestrator walk-backs 5 weeks Phase 4 Teams + Labour 23 – 30 Sep sprint shape → service shape hiring filter lock-in 2 weeks Phase 5 Close 7 Oct what could still be 1 week Each piece stands on its own. Together they are the magazine the next ordinary install actually deserves.

The Point

The 700 MB are part of the problem. So is the rest of what follows. Four things broke at once, each defensible at the time it was introduced, each compounded by the next, each kept in place by an industry that has no interest in reducing it. The result is not a system that occasionally fails. It is a system that is, on its own terms, unauditable: large where it could be small, ungoverned where it could be bounded, oddly shaped where it could fit the machine, layered where it could be plain.

This is what the rest of the Bow will take, one Wednesday at a time. The first cut, not the last.

Next Wednesday: the count, and why none of it was justified.