· 7 min read ·

The Frontend Tax: Sorting What You Chose From What You Had To

Source: lobsters

Fred Brooks drew a line in “No Silver Bullet” back in 1986 that the software industry has been arguing about ever since. Essential complexity is inherent to the problem you are solving; it cannot be removed without changing what the problem is. Accidental complexity is what your tools, processes, and conventions add on top. Brooks thought most of the hard work in software was essential, and that most tools were chasing accidental complexity gains while the essential difficulty remained untouched. The binaryigor post on modern frontend complexity applies that lens to the contemporary JavaScript ecosystem, and it is a useful exercise. But the interesting question is not whether the complexity exists. It obviously does. The interesting question is which parts you actually had to pay.

The Problem the Web Browser Poses

Building an interactive UI in a browser is genuinely hard. The DOM is a stateful, imperative tree. User events arrive asynchronously. Network requests can fail or arrive out of order. Multiple users can be looking at the same application in wildly different browser versions, on different operating systems, on screens ranging from a 320px phone to a 4K monitor. Accessibility requirements mean you have to manage focus, ARIA state, and keyboard navigation alongside visual presentation.

None of that complexity disappears if you delete your node_modules folder. A form that submits data, validates input, shows loading state, and handles errors is a genuinely complex piece of software regardless of what tools built it. A real-time collaborative editor is one of the harder distributed systems problems that exists, and the browser just happens to be where the client lives. That is essential complexity. Brooks would recognize it immediately.

So the question is what sits on top of that.

What the Toolchain Costs

A minimal React project created with Vite in early 2026 pulls in roughly 140 packages before you write a single line of application code. A Create React App project historically installed over 1,400. The Vite situation is better, but consider what those 140 packages represent: a build system, a module bundler, a JSX transformer, a dev server with HMR, TypeScript compilation, and a set of PostCSS plugins for CSS processing. None of that is your application. All of it is infrastructure for writing your application.

The gzip-compressed size of React 18 plus ReactDOM is around 45KB. Vue 3’s runtime is about 22KB gzipped. These are not enormous numbers in isolation, but they arrive before your application code does, they block rendering, and they exist even for pages where the interactivity could have been handled with 50 lines of vanilla JavaScript. Svelte takes a different stance: it compiles components into imperative DOM operations at build time, meaning the framework itself ships essentially zero runtime bytes. The output is larger per-component than something like Vue, but for small applications the total is dramatically smaller.

The webpack configuration problem deserves its own paragraph. For years, a medium-sized production application carried a webpack config that ran several hundred lines, required intimate knowledge of loaders, plugins, chunk splitting strategy, source map behavior, and tree-shaking constraints. Vite improved this significantly by leaning on native ES modules in development and using Rollup under the hood for production builds. But even Vite has configuration surface area that can grow: SSR mode, library mode, worker configuration, environment-specific plugins. The toolchain complexity has decreased, but the floor has not reached zero.

This is accidental complexity in Brooks’s sense. It exists because of the historical accident that JavaScript was designed as a scripting language in a week, that the CommonJS module system became dominant before ESM was standardized, that CSS had no scoping mechanism for component-level styles until recently, and that cross-browser compatibility required polyfilling platform capabilities that should have been there. Tooling filled the gaps left by a platform that evolved slowly, and the tools accumulated their own complexity in doing so.

State Management as a Case Study

State management is where the essential/accidental line gets blurry. Redux was introduced in 2015 and became the standard way to manage application state in React applications. It requires you to define actions, reducers, selectors, and middleware. For a to-do list, this overhead is comically large. For a financial dashboard with shared state across dozens of components, user permissions, optimistic updates, and cache invalidation, something with Redux’s structure starts to make sense.

The issue is that Redux became the default for everything, not just the applications where its constraints paid off. Then came MobX, then Zustand, then Jotai, then Recoil, then Valtio. Each solved legitimate problems with its predecessors. Zustand’s entire source is around 1KB and its API is considerably simpler than Redux’s. The churn between these libraries is accidental complexity: multiple solutions to the same essential problem, each requiring its own mental model and migration cost.

React itself evolved in a direction that reduced the essential need for external state management. The hooks introduced in React 16.8 gave components useState, useReducer, and useContext as first-class primitives. Server components in React 18 and 19 moved a significant class of data-fetching problems off the client entirely. The essential complexity of managing shared state across a large UI remains, but some of the accidental complexity that libraries were papering over has been absorbed into the framework.

The Meta-Framework Layer

Next.js, Remix, Nuxt, SvelteKit, and Astro occupy a layer above the component framework. They add routing, SSR, static generation, image optimization, and API routes. For server-rendered applications, this layer solves real problems: SEO, first-contentful-paint latency, and the cost of hydrating large client-side applications.

But the meta-framework layer also adds its own abstraction costs. Next.js has gone through several routing paradigms: file-based pages, then the App Router with React Server Components and a new mental model for layouts, loading states, and error boundaries. Each paradigm shift requires relearning conventions and often migrating existing code. The App Router introduced a distinction between server components and client components that requires developers to understand React’s execution model at a level that was not previously necessary.

This is a case where the complexity is partly essential (server rendering genuinely requires coordinating client and server) and partly accidental (the particular API shape is a design choice that could have been different, and may be different again in a future version).

What the Platform Has Fixed

The browser platform in 2026 is a meaningfully different environment from the one that made jQuery necessary, that drove the adoption of Sass, or that made Babel’s transpilation of ES6 to ES5 mandatory. Native ES modules work in every supported browser. CSS custom properties, grid, container queries, and the :has() selector have broad support. The fetch API replaced XMLHttpRequest. IntersectionObserver and ResizeObserver eliminated entire categories of scroll and layout JavaScript. The Web Animations API handles a lot of what jQuery’s .animate() covered.

This matters because a significant fraction of the toolchain complexity that accumulated in the 2015-2020 era exists to work around platform limitations that no longer apply. Babel’s preset-env with polyfills made sense when you had to support IE11. It is harder to justify today. The You Might Not Need jQuery argument from 2013 has a 2026 analog: you might not need a build step at all for applications of modest complexity.

HTMX represents one answer to this: server-rendered HTML with declarative attributes that add hypermedia behaviors, no build step required. For applications that are fundamentally about displaying and modifying server state rather than managing complex client-side interactions, the React component model may simply be the wrong tool. The essential complexity of a content-driven application with some interactive forms is low. Reaching for a framework that assumes you need client-side routing and a virtual DOM is choosing accidental complexity voluntarily.

The Useful Distinction

The point is not that frameworks are bad or that everyone should write vanilla JavaScript. React’s component model solves a real problem for large teams working on large applications: it enforces a decomposition into units that can be developed, tested, and reasoned about independently. TypeScript’s static types catch entire classes of bugs that would otherwise surface at runtime in production. Vite’s HMR makes a meaningful difference in iteration speed during development.

The useful exercise is asking, per feature and per application, which complexity you are actually paying for the problem you have. A marketing site with a contact form and some animations does not need a JavaScript bundle at all. A content-heavy publication benefits from Astro’s partial hydration model, shipping JavaScript only to components that require it. A complex SaaS application with real-time collaboration, complex authorization logic, and dozens of interactive screens may justify every layer of the React plus TypeScript plus Next.js stack.

The failure mode is treating the full stack as the default and then trimming. The default should be the minimum that handles the essential complexity of your specific problem. Every piece of toolchain you add should pay its cost in developer productivity or user experience, concretely, for your application, not in theory for some hypothetical scale you have not reached.

Brooks’s insight is that essential complexity is irreducible, and tools that claim to eliminate it are selling something. But he also said that the accidental complexity was where tools could genuinely help. The frontend ecosystem has spent a decade oscillating between adding accidental complexity and then building tools to manage the complexity it added. Recognizing which parts of your stack fall into which category is the practical version of the question.

Was this interesting?