Vivian Voss

Vanilla JS: The Framework You Already Have

javascript web performance

Lean Web ■ Episode 6

“We need React for this.”

One hears the sentence in stand-ups, architecture reviews, sprint plannings, and that particularly charged moment when a project lead opens a whiteboard and draws boxes. It arrives with the quiet confidence of a fact. It is, in most cases, nothing of the sort.

DOM manipulation. Event handling. State updates. HTTP requests. JavaScript solved every one of these in 2015. Not with a framework. Not with a library. With the language itself and the browser APIs that ship in every browser on every device, at zero cost, with no build step, no transpilation, and no dependency tree.

The question is not whether React works. The question is what precisely it does that the browser does not already do for free.

Five Methods

The browser’s native API surface is vast. But for the overwhelming majority of interactive websites (not applications, websites) the daily work reduces to five methods:

document.querySelector(): find an element.
element.addEventListener(): respond to an event.
element.classList.toggle(): change appearance.
fetch(): make an HTTP request.
new WebSocket(): maintain a persistent connection.

Five methods. No imports. No configuration. No package.json. No node_modules. No build pipeline. The browser understands them natively, executes them immediately, and has done so, reliably, for over a decade.

React, by contrast, requires JSX transpilation, a bundler (Webpack, Vite, or similar), Node.js as a build dependency, a package.json with a median of 47 direct dependencies, a node_modules directory containing roughly 1,200 folders, a build step that produces a bundle, and a hydration phase that reconstructs on the client what the server already rendered.

The output of this ceremony is a DOM update. The same DOM update that five native methods perform without ceremony at all.

REACT VANILLA JS JSX Babel / Transpiler Webpack / Vite Node.js package.json (47 deps) node_modules (1,200) Build → Bundle → Ship DOM Update vs <script> querySelector, fetch, etc. DOM Update 8 steps, 142 KB framework 1 step, 0 KB framework

“But Components!”

The first objection arrives like clockwork. “React gives us components. Reusable, encapsulated, composable.” This is true. It is also true that the browser has offered precisely the same capability since 2020, without React, without a build step, and without a single line of JSX.

Web Components, specifically Custom Elements, are a browser-native standard. A class extending HTMLElement, a connectedCallback, and customElements.define(). Three constructs. Native encapsulation via Shadow DOM. Native lifecycle hooks. Native attribute observation. No transpiler required. No virtual DOM. No reconciliation engine.

Browser support stands at 98% globally, which is higher than the support for several CSS features that nobody hesitates to ship.

The distinction is important. React components are a React abstraction. They exist inside React, render through React, and cannot escape React without a wrapper. Web Components are a platform primitive. They work in React. They work in Vue. They work in a static HTML file served from a cupboard. They work because the browser understands them, not because a framework permits them.

“But Reactivity!”

The second objection follows promptly. “React gives us reactivity. Change the state, the view updates automatically.” This is, once again, correct. It is also achievable in eight lines of vanilla JavaScript.

The Proxy object, standardised in ES2015 and supported in every modern browser, intercepts property access and mutation on any JavaScript object. Wrap your state in a Proxy, define a set trap that calls a render function, and you have reactivity. Real reactivity. Not virtual-DOM-diffing reactivity. Direct, surgical DOM updates triggered by the actual mutation, with no intermediate tree comparison.

React’s reactivity model builds a new Virtual DOM tree on every state change, diffs it against the previous tree, calculates the delta, and patches the real DOM. Four operations to achieve what a Proxy achieves in one. The overhead is not theoretical. It is measured in milliseconds, in CPU cycles, and in the battery life of every mobile device that must perform the calculation.

The Numbers

Let us be specific about what ships to the user.

WHAT SHIPS TO THE USER minified + gzipped transfer size React + ReactDOM 142 KB Vanilla JS 0 KB Web Components 0 KB Proxy 0 KB Source: bundlephobia.com/package/react-dom ■ caniuse.com

React plus ReactDOM: 142 KB, minified and gzipped. That is the transfer size; the browser decompresses and parses considerably more. Before your application renders a single <div>, 142 kilobytes of framework must arrive, decompress, parse, and execute.

Vanilla JavaScript: 0 KB. It is already in the browser. It shipped with the browser. It is the browser.

Web Components: 0 KB. Same reason. Custom Elements are a browser specification, not a library.

Proxy: 0 KB. It has been part of the JavaScript engine since 2015.

Zero is a compelling bundle size. It is difficult to optimise further. Tree-shaking zero kilobytes yields, one imagines, zero kilobytes. Code-splitting zero kilobytes produces, remarkably, zero kilobytes. The performance budget for the browser’s native APIs is settled before the conversation begins.

The Ecosystem Paradox

React’s ecosystem is enormous. Redux, React Router, React Hook Form, Formik, React Query, Zustand, Jotai, Recoil. The constellation of libraries is vast, well-maintained, and deeply impressive. It is also, structurally, a consequence of what React does not do.

React does not manage global state. So the ecosystem produced Redux, Zustand, Jotai, Recoil, MobX, and Valtio. React does not handle routing. So the ecosystem produced React Router, TanStack Router, and Next.js. React does not manage forms natively. So the ecosystem produced React Hook Form, Formik, and Final Form. React does not animate. So the ecosystem produced Framer Motion and React Spring.

Each library solves a real problem. The question is who created the problem.

The browser has addEventListener for events, the History API for routing, FormData for form handling, the Web Animations API for animation, and CustomEvent for cross-component communication. These are not experimental features behind a flag. They are shipping standards, implemented in every major browser, with years of stability behind them.

React’s ecosystem does not extend the platform. It replaces the platform. And the replacement costs 142 KB before it begins to function, plus whatever the ecosystem libraries add on top.

The jQuery Parallel

The industry has been here before. In 2006, jQuery arrived and solved genuine problems: inconsistent DOM APIs across browsers, missing selector engines, unreliable event handling. It dominated for a decade. Then the browsers caught up. querySelector replaced Sizzle. addEventListener replaced $.on(). fetch replaced $.ajax(). The gap closed. jQuery became unnecessary.

Not overnight. Not dramatically. Gradually, and then completely. W3Techs still reports jQuery on 77% of websites, but the trajectory is unambiguous and the direction is down. New projects do not start with jQuery. The library persists because legacy persists, not because the need persists.

React’s trajectory is structurally identical. It solved real problems in 2013: the DOM API was verbose, state synchronisation was manual, and component encapsulation required discipline the platform did not enforce. In 2026, the browser offers querySelector, Proxy, Web Components, fetch, AbortController, MutationObserver, and the Web Animations API. The gap is closing. The same gap, the same direction, the same conclusion.

The only question is the timeline. jQuery took roughly a decade. React may take longer, its integration into build tooling, hiring pipelines, and university curricula creates a structural inertia that jQuery never achieved. But inertia is not necessity. And “We have always used React” is not an architectural argument. It is a habit dressed in a tech stack.

Where React Earns Its Keep

This is not, to be tediously clear, a call to abolish React. The framework solves genuine problems for genuine applications. Google Docs. Figma. Spotify’s web player. Dense, stateful, interactive surfaces where users remain for hours and the framework cost amortises across thousands of interactions. Where client-side state management is not a convenience but a structural necessity. Where the Virtual DOM’s reconciliation model earns its overhead by managing complexity that would be unmanageable without it.

These applications exist. They represent, at a generous estimate, perhaps 5% of what gets built with React. The remaining 95% are marketing sites, blogs, documentation portals, product catalogues, and landing pages, documents with the occasional interactive element, served to browsers that handle documents natively and have done so for three decades.

The Invoice

What you pay for React: 142 KB of framework. A build pipeline (JSX, Babel, Webpack/Vite, Node.js). A node_modules directory with 1,200 folders. A hydration phase that rebuilds what the server already rendered. A Virtual DOM that diffs what the real DOM already knows. An ecosystem of libraries that replaces what the browser already provides.

What the browser provides for free: querySelector, addEventListener, classList, fetch, WebSocket, CustomEvent, FormData, AbortController, MutationObserver, Proxy, Web Components, the Web Animations API, and the History API. All native. All without a build step. All shipping in every browser since at least 2020.

React “Hello World” loads 142 KB before your code. Browser “Hello World” loads your code.

The fastest framework is the one you do not ship. And the most reliable dependency is the one that shipped with the browser.