Skip to content

The Framework Tax

Frameworks aren’t free. They never were, but the cost has changed shape over the last decade.

In 2014 the framework tax was bundle size and learning curve. You paid a few hundred kilobytes and a week of onboarding to get a productivity bump that was, honestly, worth it. The browser couldn’t do components, couldn’t do scoped styles, couldn’t do reactive state. You needed help.

In 2026 the tax is different and larger. Three pieces stack up.

1. Render-time virtualization you no longer need

Section titled “1. Render-time virtualization you no longer need”

Modern frameworks insert a layer between your code and the DOM. JSX → virtual DOM → reconciliation → real DOM. Vue and Svelte have analogues. The cost is paid on every render and accumulates on every re-render.

The original justification was: we can’t trust developers to write efficient DOM updates. That made sense when “efficient DOM updates” meant manual appendChild calls and reflow management. It is much less true now. Modern Lit, vanilla custom elements, and even careful innerHTML swaps are competitive with virtual DOM diffing for the apps most teams actually ship — at a fraction of the runtime cost.

You can verify this yourself: load a typical React dashboard, open the performance profiler, and look at what time is spent in. A meaningful slice is the framework reconciling itself. You paid for that.

Server-rendered HTML used to be cheap. Now it’s a multi-step dance:

1. Server renders HTML.
2. Browser receives HTML, parses it, paints it.
3. Browser downloads the framework runtime.
4. Browser downloads your app code.
5. Framework re-runs your component tree against the existing DOM.
6. Framework attaches event handlers.
7. Page becomes interactive.

Steps 3-6 are hydration. They exist because the framework owns the DOM and needs to take ownership back from the server-rendered version. Without step 5, your event handlers don’t fire. Without step 4, the framework can’t run.

Custom elements skip this. The browser already knows how to upgrade <kit-button> when its definition arrives. There’s no hydration step because there’s no parallel DOM tree to reconcile. The component is the element; the element is in the page; the user can already click.

The platform has a deeply considered accessibility model. Roles, names, focus order, keyboard activation, form participation — the browser does most of this for you, if you use the right elements.

Frameworks tend to abstract those elements away. A <Button> is rarely a <button> in disguise — it’s often a <div> or a <span> with an onClick. Native focus and keyboard semantics are gone. They get re-added, partially, through tabIndex, role, and onKeyDown. Sometimes correctly. Often not.

Look at any popular framework component library and count how many of the “accessible” components actually use the native primitive. Buttons are the most common offender. Dialogs are the second. Form fields are the third.

When the browser already gives you the answer, replacing it is an accessibility regression in waiting.

The honest accounting:

  • 150–300 KB of framework runtime, before you write any code.
  • Hydration overhead on every initial load that the platform doesn’t require.
  • An accessibility surface area that has to be re-implemented per component, instead of inherited from the browser.
  • A version-upgrade cadence dictated by the framework’s release schedule, not your product’s needs.
  • A talent market that expects to write framework code, not platform code, which means hiring becomes a framework-loyalty signal more than a competence signal.

You can be paid for this tax, in productivity and ergonomics, and for many teams that’s a fair trade. But it should be a conscious trade. The browser used to be unable to do this work. It can now.

Kitsune isn’t a competing framework. There’s no virtual DOM. There’s no reconciler. The runtime is small enough to read in one sitting (≈ 250 lines). Lit handles component authoring; the rest is plain TypeScript and plain DOM.

What you write looks like this:

<kit-shell>
<kit-boundary surface="page">
<kit-button meta-event="thing.happened">Thing</kit-button>
</kit-boundary>
</kit-shell>

What runs in the browser is approximately that, with the elements upgraded by the platform’s native custom-element machinery.

You stop paying the tax. You also stop getting some of the things the tax pays for — convention-driven directory structure, CLI scaffolding, a giant ecosystem of opinionated plugins. Whether that’s a good trade depends on your app. For long-lived applications, it almost always is.

Next: What We Lost →