The Landscape

How the big frameworks solve the same problems

Every UI framework solves three problems: reactivity, binding, and reconciliation. They just solve them differently.

Understanding these differences IS understanding modern web dev.

Framework Reactivity Binding Reconciliation
React useState + scheduler JSX compilation Fiber + Virtual DOM
Vue 3 Proxy-based ref/reactive Template compiler Virtual DOM diff
Svelte Compile-time analysis Compile-time Surgical DOM ops
Solid Fine-grained signals JSX + reactivity No VDOM, direct updates
Alpine Proxy (like qrazy) Runtime attributes Simple re-render
htmx Server is the state HTML responses Replace HTML chunks
qrazy Proxy Runtime attributes Keyed Map

The same counter, six ways

Let's implement a simple counter in each framework. Same functionality, different philosophies.

React

"UI as a function of state"
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Clicked {count} times
    </button>
  );
}

How it works: useState returns a value and a setter. Calling the setter schedules a re-render. React re-runs the entire function, builds a virtual DOM tree, diffs it against the previous tree, and patches the real DOM.

Pros: Predictable mental model. Pure functions. Great devtools. Huge ecosystem.
Cons: Re-renders entire component on any state change. Virtual DOM overhead. Requires build step.

Vue 3

"Progressive framework"
<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>

<template>
  <button @click="count++">
    Clicked  times
  </button>
</template>

How it works: ref() wraps a value in a Proxy. The template compiler tracks which parts of the template depend on which refs. When a ref changes, Vue re-renders only the affected parts via virtual DOM diffing.

Pros: Fine-grained Proxy reactivity. Clean template syntax. Good TypeScript support. Can be used without build step.
Cons: Need .value to access refs in JS. Two APIs (Options vs Composition). Virtual DOM still has overhead.

Svelte

"Compiler does the work"
<script>
  let count = 0;
</script>

<button on:click="{() => count++}">
  Clicked {count} times
</button>

How it works: The Svelte compiler analyzes your code at build time. It knows exactly which DOM nodes depend on count. The output is vanilla JS that directly updates those specific text nodes. No runtime framework, no virtual DOM.

Pros: Smallest bundle size. Fastest runtime. Cleanest syntax. No virtual DOM overhead.
Cons: Requires build step. Compiler errors can be confusing. Smaller ecosystem than React/Vue.

Solid

"True fine-grained reactivity"
import { createSignal } from 'solid-js';

function Counter() {
  const [count, setCount] = createSignal(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Clicked {count()} times
    </button>
  );
}

How it works: Signals are reactive primitives. When you read count() inside JSX, Solid tracks that dependency. When you call setCount(), only the exact text node that reads count() updates. No re-render, no diffing.

Pros: React-like syntax with Svelte-like performance. True fine-grained updates. Components run once, not on every update.
Cons: Must call signals as functions count(). Mental model shift from React. Smaller ecosystem.

Alpine

"jQuery for the modern web"
<div x-data="{ count: 0 }">
  <button @click="count++">
    Clicked <span x-text="count"></span> times
  </button>
</div>

How it works: Alpine scans the DOM at runtime for x-* attributes. It wraps the data in a Proxy and re-evaluates bindings when state changes. Very similar to qrazy—we basically built a mini-Alpine.

Pros: No build step. Sprinkle on existing HTML. Locality of behavior. Great for server-rendered apps.
Cons: Runtime parsing overhead. All data in HTML attributes. Not ideal for complex SPAs.

htmx

"Hypermedia as the engine"
<!-- Client -->
<button hx-post="/increment"
        hx-swap="innerHTML">
  Clicked 0 times
</button>

# Server (returns HTML, not JSON)
POST /increment
return "Clicked 1 times"

How it works: htmx intercepts user actions and makes HTTP requests. The server responds with HTML, not JSON. htmx swaps that HTML into the page. The server IS the state—no client-side state management needed.

Pros: No client-side state. Server renders everything. Works with any backend. Simple mental model.
Cons: Network latency on every interaction. Server does more work. Not ideal for offline/realtime apps.

qrazy

"Learn by building"
<div q-data="{ count: 0 }">
  <button q-click="count++">
    Clicked <span q-text="count"></span> times
  </button>
</div>

How it works: Almost identical to Alpine. Proxy-based reactivity, runtime attribute parsing, stored updater functions. 250 lines of code you can read in an afternoon.

Pros: You understand every line. No magic. Great for learning. Actually works.
Cons: Not production-ready. No component system. No SSR. Limited features.

The meta-lesson

Each framework makes tradeoffs:

  • Build step vs runtime: Svelte analyzes at build time for speed. Alpine/htmx work with zero tooling.
  • Virtual DOM vs direct updates: React/Vue use VDOM for predictability. Solid/Svelte update DOM directly for performance.
  • Client state vs server state: React/Vue manage state in the browser. htmx keeps it on the server.
  • Bundle size vs features: Alpine is 15KB. React is 45KB+. You're paying for the feature set.

There's no "best" framework. There's only the best framework for your situation. Understanding the tradeoffs lets you make informed choices.