Vivian Voss

The Framework Tax

javascript web architecture

Performance-Fresser ■ Episode 11

In 2010, a competent developer could build 95% of web applications with vanilla JavaScript, a text editor, and the quiet confidence that the browser would execute what was written. No compilation. No bundling. No dependency tree sprawling into the thousands. The code you wrote was the code that ran.

In 2026, a React Hello World application installs 2,839 packages. Two thousand, eight hundred and thirty-nine. For a function that returns a heading tag.

One might reasonably ask what happened in between. The answer is not complicated. It is merely expensive.

The Dependency Timeline

The trajectory from self-sufficiency to structural dependency took roughly fifteen years. It did not arrive all at once. It arrived in increments, each one presented as an improvement, each one adding weight that would never be removed.

0 1,000 2,000 3,000 packages installed 0 ~8 ~850 ~1,700 2,839 2010 2014 2018 2022 2026 Vanilla JS jQuery + plugins React + tooling Full modern stack Hello World Source: npm, bundlephobia.com

Create React App, the official starter template, the blessed path for beginners, produces a node_modules directory exceeding 200 MB. For a starter template. For an application that renders one line of text. The directory that contains your dependencies weighs more than the operating system that ran the Apollo guidance computer, and one suspects the Apollo computer was doing slightly more interesting work.

2,839 Trust Decisions

Each dependency is a trust decision. You are executing code written by a stranger, on your machine, with your permissions. That is the transaction. Dress it up however you like (ecosystem, community, open source) but the mechanics are identical to downloading an executable from the internet and running it without inspection.

A direct dependency is a trust decision you made. A transitive dependency is a trust decision someone else made on your behalf, without your knowledge, and quite possibly without theirs. When npm install pulls in 2,839 packages, you have consciously evaluated perhaps a dozen. The remaining 2,827 are running on faith.

npm install is remote code execution. Postinstall scripts run with your permissions. The only difference between this and malware is the presumption of good intent.

And as it happens, the presumption of good intent has not held up terribly well.

The Casualty List

Supply chain attacks against npm are not theoretical. They are not edge cases. They are a recurring pattern with escalating ambition, and every incident follows the same structural logic: find a package that thousands of projects trust implicitly, and replace the trust with exploitation.

event-stream, 2018. A package with 1.5 million weekly downloads. The original maintainer, exhausted, transferred publishing rights to a stranger who asked politely. The new maintainer injected Bitcoin-stealing malware. It went undetected for months. Not days. Months. An entire quarter of the calendar year during which production applications worldwide were silently compromised.

ua-parser-js, 2021. Seven million weekly downloads. The maintainer’s account was hijacked, and the package was replaced with a version that installed a cryptominer and a credential stealer. The original package parses user-agent strings. The compromised version mined cryptocurrency and harvested passwords. Detected after four hours, which sounds fast until one considers how many npm install commands execute in four hours across the planet.

node-ipc, 2022. A dependency of Vue.js. The maintainer did not have his account stolen. He sabotaged his own package deliberately, deploying code that deleted files from developer machines based on geolocation. Protestware, as the industry has euphemistically labelled it. The supply chain weaponised not by an external attacker but by the person you trusted to maintain it.

colors.js and faker.js, 2022. The maintainer of two enormously popular packages intentionally sabotaged both, pushing updates that caused applications worldwide to crash with infinite loops and garbled output. His motivation was frustration that Fortune 500 companies were using his work without compensation. One might sympathise with the sentiment whilst noting that the remedy was indistinguishable from an attack.

event-stream (2018) 1.5M weekly downloads • maintainer access transferred • Bitcoin-stealing malware Undetected for months ua-parser-js (2021) 7M weekly downloads • account hijacked • cryptominer + credential stealer Detected after 4 hours node-ipc (2022) Vue.js dependency • maintainer sabotaged own package • files deleted from dev machines Protestware: the supply chain weaponised from within colors.js / faker.js (2022) Intentional sabotage • infinite loops • applications worldwide crashed

Four incidents in four years. Account theft, social engineering, deliberate sabotage, ideological protest. Four different vectors, one shared root cause: the dependency tree is a trust chain, and every link you did not personally inspect is a link that can be replaced.

The Build Step Tax

The framework does not merely add dependencies. It adds an entire intermediate layer between the code you write and the code the browser executes. Your source passes through Webpack, or Vite, or esbuild. It is processed by Babel. Types are stripped by TypeScript. Styles are transformed by PostCSS. The result is not your code. It is a derivative of your code, produced by a pipeline whose individual components you understand imperfectly and whose interactions you understand less.

When something breaks in production, you are not debugging your application. You are debugging the output of a toolchain that has opinions about how your application should look after transformation. The stack trace points to minified identifiers. The line numbers correspond to a generated file. The source map (if it exists, if it is current, if it has not drifted) is your only bridge back to what you actually wrote.

In 2010, the code you wrote was the code the browser ran. In 2026, the code you write is a suggestion, and the toolchain decides what arrives.

The Bundle

Before you write a single line of application code, the framework has already committed you to a baseline payload. The user’s browser must download, parse, and execute the framework itself before your application can render so much as a heading tag.

Framework baseline: before your code 0 KB 50 100 150 React 18 136 KB 44 KB gzip Vue 3 126 KB 33 KB gzip Angular 17 180 KB 62 KB gzip Native APIs 0 KB already in the browser Source: bundlephobia.com

React 18: 136 KB. Vue 3: 126 KB. Angular 17: 180 KB. These are the framework sizes alone, before a single line of application code. Gzipped, the numbers compress to 44, 33, and 62 KB respectively, which sounds more palatable until one remembers that the browser must still decompress, parse, and execute the full uncompressed payload. Gzip reduces transfer time. It does not reduce parse time, and parse time is where mobile devices lose the argument.

The Hydration Farce

Server-side rendering was supposed to be the answer. The server renders HTML. The browser displays it instantly. Fast. Accessible. Done.

Except it is not done. The framework must now “hydrate” the page: attach event listeners, reconstruct the component tree, reconcile the virtual DOM with the actual DOM. The server rendered the page. The client re-renders it. Two full render passes to achieve what a <button onclick=“handleClick()”> achieves natively, in one pass, with zero JavaScript framework overhead.

The industry has invented partial hydration, progressive hydration, islands architecture, resumability, an entire taxonomy of workarounds for a problem that only exists because the framework created it. One does not need a sophisticated solution to the hydration problem if one does not have a hydration problem. And one does not have a hydration problem if one does not insist on re-implementing the browser’s native capabilities in JavaScript.

The Native Alternative

The browser in 2026 is not the browser of 2010. It ships with an extraordinary set of native APIs, all of them free, all of them zero-dependency, none of them requiring a build step:

<template>: native HTML templating, parsed but not rendered until explicitly stamped into the DOM. No virtual DOM required.

Web Components: customElements.define(), Shadow DOM, HTML slots. Encapsulated, reusable components with native browser support. No framework required.

addEventListener(): event delegation has been available since the DOM Level 2 specification. It handles click events without downloading 136 KB of reconciliation logic.

The History API: pushState() and popstate provide client-side routing without a routing library, without a framework, without a single additional byte.

MutationObserver: watches the DOM for changes and reacts programmatically. The reactive pattern that frameworks re-implement, available natively since 2012.

Every one of these has been supported in all major browsers for years. Every one of them costs zero kilobytes. Every one of them runs without a build step. The browser is not lacking in capability. The industry is lacking in willingness to use what the browser already provides.

The Compound Invoice

The framework tax is not a single line item. It compounds. The framework requires a bundler. The bundler requires configuration. The configuration requires expertise. The expertise requires hiring. The hiring requires salary. The salary requires revenue. The revenue requires users. And the users are waiting for the 180 KB of Angular to finish parsing on their mid-range Android device, wondering why a page that contains six product cards takes four seconds to become interactive.

In 2010, the web was a document platform that could do interactive things. In 2026, it is an application platform that struggles to do document things. The industry took a medium that was fast by default and made it slow by choice, then spent fifteen years building tooling to claw back the performance it had voluntarily surrendered.

Two thousand, eight hundred and thirty-nine packages. For Hello World.

The invoice is in your node_modules folder. You might want to read it.