Twelve Frontend Paradigms in 2026: Field Notes from Building My Own Language
A survey of how modern web UIs are actually built — VDOM, signals, compile-time reactivity, web components, server-driven, FRP, islands, resumability, state machines, functional UI — collected while deciding what frontend story my own programming language should aim for.
Why I went looking
I’m building my own programming language. It’s small, ML-flavored, with algebraic effects, capability passing, and a region-based memory model. Once the core stabilizes, I want it to compile to WebAssembly so it can also drive web UIs.
The naive plan is “reimplement React in my language.” That’s a clean story for a blog post, but as a design choice it bothered me. React was designed in 2013. The VDOM diff model that defined it has been losing benchmarks for years, and most of the languages I’d want to compete with — Solid in JS, Leptos in Rust, Bonsai in OCaml — have already moved on.
So I sat down to map out what people actually build web UIs with in 2026, and what trade-offs each approach is making. This note is that survey, written up for myself as much as for anyone else.
A note on bias: I’m a backend / language person looking at frontend. I’m not the right person to defend any of these communities. But the design space matters to me, because the language I’m building has to make a choice — or, more accurately, has to make several choices in sequence.
The twelve paradigms
There are more than the canonical “React vs Vue” dichotomy. After several passes I converged on twelve. The boundaries are fuzzy — Vue 3 sits between #1 and #2, Svelte 5 is moving from #3 toward #2 — but the cluster centers are real.
1. Virtual DOM + diff
Examples: React, Vue 2, Preact, Inferno
The original “modern” frontend story. UI is a pure function state → vdom tree. When state changes, you regenerate the whole tree, diff it against the previous one, and patch only the differences into the real DOM.
The mental model is simple: think of UI as a function of state. Re-render the world, the framework figures out the minimum DOM mutation. For a long time, this was the dominant teaching narrative — even non-React frameworks adopted “components that take props and return JSX-ish output.”
The cost is that you’re building a tree every time, then walking two trees to compare them. That’s GC pressure and CPU time that doesn’t actually move the screen. The runtime is also substantial: React + ReactDOM together are around 40KB minified.
What killed pure VDOM as the leading edge isn’t that it broke — apps shipped fine. It’s that newer designs do the same job with less work, and React’s own internal direction (Server Components, compilers) is partly an attempt to escape its original premise.
2. Fine-grained reactivity / signals
Examples: Solid.js, Vue 3, Leptos (Rust), Angular Signals, Preact Signals, Knockout (the historical ancestor)
Instead of “rebuild the world when state changes,” each piece of state is a signal — a value you can read, write, and subscribe to. When you read a signal inside a render function, the framework records that the function depends on that signal. When the signal changes, only the DOM nodes that read it are updated.
There’s no VDOM and no per-component re-render. There’s also no manual useMemo or useCallback — the dependency graph is built automatically by tracking reads.
This wins essentially every JavaScript framework benchmark in 2026. Solid is at the top of the Krausest js-framework-benchmark; Vue 3, Leptos, Angular Signals, Preact Signals, and Svelte 5 are all heading in the same direction. If there’s a single trend that defines mid-2020s frontend, it’s “everyone is moving to signals.”
The trade-off is conceptual: signals are reference-passed by default. count in Solid isn’t a number, it’s a getter. Code that looks like JavaScript but isn’t trips people up the same way React’s stale-closure problem did in its early years.
For my language, this is the most interesting target. Capability passing maps onto signal passing almost directly. The dependency graph wants a type system that can track effects, which is exactly what I’m building.
3. Compile-time reactivity
Examples: Svelte (1–4), Marko
What if the framework didn’t ship a runtime at all, and instead the compiler turned ordinary-looking code into surgical DOM updates?
In Svelte, you write let count = 0; count += 1 and the compiler emits the DOM update calls for you. There’s no signal type, no useState, no proxy. The output bundle is tiny — Svelte 3 ships in roughly 3KB.
The catch is that the compiler becomes the magic. The behavior of a line of code depends on what the compiler decides to instrument. That’s fine when it works and confusing when it doesn’t, especially for dynamic patterns the compiler can’t see (state created in a loop, for instance).
Svelte 5 quietly conceded part of this fight by introducing runes — explicit reactive primitives that look a lot like signals. The compile-time-only ideal turned out to be hard to push past medium-complexity apps. But the underlying idea — “your compiler is a frontend optimization opportunity” — is correct and underused.
For a language with its own compiler, this is interesting territory. The compile-time approach maps onto the kind of macro / staged-compilation work my language wants to do anyway.
4. Web Components + tagged templates
Examples: Lit, FAST, Stencil
Use the browser’s actual primitives: Custom Elements, Shadow DOM, slots. Wrap them in a small library that gives you a html\…`` template literal, parse the template once, and update only the dynamic holes.
The pitch is “stop reinventing what the browser already does.” A Lit component is a real HTMLElement. You can use it from React, from Vue, from vanilla — it’s a primitive of the platform, not of the framework.
This paradigm is less visible in public discourse but heavy in enterprise. Apple, Google, Adobe, IBM all use Web Components for design systems. Lit’s bundle size is tiny, and “framework-agnostic” is a serious feature when you’re shipping a component library that other teams will consume across stacks.
The downside is that reactivity is shallow. Lit gives you @property decorators that mark fields as reactive, but you’re closer to “imperative DOM with sugar” than to fine-grained reactivity. For a small surface, this is fine. For complex state graphs, you start wanting more.
5. Server-driven UI
Examples: HTMX, Hotwire (Turbo + Stimulus), Phoenix LiveView, Blazor Server, Inertia.js
The client is dumb. It renders HTML. Every interaction sends a request, and the server returns more HTML — sometimes a whole page, sometimes a fragment that gets swapped into the DOM by an attribute like hx-target. LiveView and Blazor Server keep a WebSocket open and push partial updates from the server in real time.
The thing that changed in the last few years isn’t the technique — server-rendered HTML is older than JavaScript — but the realization that for a lot of apps it’s the right answer. HTMX in particular has become a kind of rallying point for “frontend complexity is a choice we don’t have to make.” The library itself is 14KB; the rest is just HTML and your existing backend.
For language builders, this is the easiest frontend story by far: you don’t actually need to ship a UI framework. You ship a good server-side language and a templating story, and HTMX or LiveView-style attributes do the rest. The interaction model becomes “request → HTML response → DOM swap” and the language is allowed to stay backend-shaped.
The cost is that everything is a round-trip. Offline doesn’t work. Optimistic UI is hard. For a CRUD-shaped product, this is great. For a tool that needs to feel local — drawing app, IDE, real-time game — it’s the wrong choice.
6. FRP (Functional Reactive Programming)
Examples: Elm, Cycle.js, RxJS as a partial approximation
Treat UI as a value over time. Events become streams. Streams compose with map, filter, merge, switch. The UI at any moment is a function of the current state of the stream graph.
This is the academic version of reactivity. It’s beautiful when it works and brutal when it doesn’t. The Elm community refined a more restricted dialect — The Elm Architecture — that swapped raw streams for an explicit Msg type and an update function, which turned out to be much easier to teach.
Outside of Elm and a few PureScript / OCaml libraries, pure FRP didn’t take over the market. But its ideas are everywhere: signals are essentially “behaviors” from the FRP literature, and the dependency tracking model in Solid descends directly from FRP work in the 90s and 2000s.
For an ML-flavored language, this is the home court advantage. The type system handles stream composition naturally, effect systems handle the impure parts cleanly, and the resulting code is provably more local in its reasoning. The catch is the market: very few people want to write production FRP, and the ones who do already use Elm.
7. Imperative + helper
Examples: jQuery, vanilla JS, Alpine.js
Grab the DOM node, mutate it. Wire up an event listener. Don’t ship a framework.
It’s tempting to file this as obsolete, but the truth is murkier. A lot of working production code — internal tools, CMS-driven pages, things added on top of a Rails or Django app — is still some flavor of “imperative DOM with a helper library.” Alpine.js explicitly designed itself to be the jQuery for the 2020s: a few KB, sprinkle attributes on HTML, get reactivity for free.
For language builders, the imperative-plus-helper path is also the natural early stage. You don’t get fine-grained reactivity for free. The first thing you can ship is “WASM module exports a set_text function, JS bridges to the DOM.” That’s where my language will start.
8. Canvas / WebGL / WebGPU
Examples: Figma, Pixi.js, three.js, Excalidraw, parts of Google Maps and Earth
Skip the DOM entirely. Get a canvas. Draw pixels. If you need 3D or heavy 2D, use WebGL or WebGPU and let the GPU do the work.
This is a different game from the other paradigms. You’re not trying to be a “framework” — you’re building your own UI runtime. Figma’s text layout, selection, hit testing, IME handling — all of that is custom code on top of a canvas. The reason they do this is that the DOM is the bottleneck. At Figma scale, “let the browser layout a million nodes” isn’t viable; “render the visible viewport ourselves at 60fps” is.
WebAssembly is a strong fit here, because the canvas-based path is much more amenable to porting an existing C++ or Rust codebase than the DOM-based path. Figma is, famously, a WASM C++ application that happens to live in a browser.
For my language, this is a long-tail option. Once WASM works and the language can talk to WebGL, drawing-app-shaped products become viable. But it’s not the path to a general-purpose web framework — it’s the path to a specific class of application.
9. Islands Architecture
Examples: Astro, Iles, Fresh (Deno), Eleventy + Alpine
Ship the whole page as static HTML. Then, for the small parts that need interactivity — the search box, the cart button, the comment form — hydrate them individually. Each “island” runs its own framework instance. The rest of the page is just HTML, never touched by JS.
This is one of the bigger trend shifts of the past few years. The realization is that for content sites — blogs, docs, e-commerce listings — the SPA model was overkill. Most of the page doesn’t change. Why ship a runtime for it?
Astro made this concrete and shippable, and the model has been broadly imitated. React Server Components is a more aggressive version of the same instinct: most of your UI tree never needs to be interactive, so don’t pay for it on the client.
For language builders, the islands model is interesting because it doesn’t require you to build a “framework” in the React sense. You build a static site generator, plus a way to embed small interactive components. The interactive parts can themselves use any of the other paradigms.
10. Resumable / serializable (Qwik)
Examples: Qwik
The frontier. Render the entire app on the server. Serialize not just the HTML but also the JavaScript state — closure captures, event handlers — into the HTML itself. The client loads the HTML and is immediately interactive: no hydration, no JS execution, no “load the framework.” Only when you click something does it lazy-load the handler for that specific button.
The promise is “constant-time time-to-interactive regardless of app size.” A 5MB Qwik app starts as fast as a 50KB one because the client doesn’t execute most of it until needed.
The implementation is a heroic compiler. Qwik chops your code into thousands of tiny chunks, each individually fetchable. Every closure becomes a serializable object. The level of compiler integration required is high enough that, for now, Qwik is largely Qwik-only — other frameworks haven’t replicated the trick.
For my language, this is the most ambitious target. The compiler control I’d have is enough to attempt resumability. But the engineering cost is large and the audience small.
11. State machine driven
Examples: XState, Robot, Stately.ai
UI is a finite state machine. State transitions are declared explicitly. Views are derived from the current state. Impossible states become unrepresentable.
This rarely shows up as “the framework.” It’s usually a layer on top of React or Solid for managing complex stateful flows — wizards, checkout, video player UIs, anywhere the question “what state can the user actually be in?” matters more than “what does this look like?”
For an ML language with sum types and pattern matching, state machines fall out for free. A type State = Idle | Loading | Loaded of Data | Error of Reason with an update : State → Msg → State function is roughly The Elm Architecture, and it’s a natural fit.
12. Functional UI (the ML family)
Examples: Elm, Halogen (PureScript), Mint, Bonsai (OCaml, by Jane Street)
The Elm Architecture, generalized. A Model holds all state. A view : Model → Html Msg renders it. An update : Msg → Model → Model handles changes. Side effects are values (Cmd) returned from update, executed by the runtime.
This is the ML community’s answer to UI, and it has the strongest claim to being “the principled version.” Bonsai, used heavily inside Jane Street, demonstrates that you can build large, real, internal-tool-grade UIs this way in OCaml. Elm’s “no runtime exceptions” claim isn’t marketing — the type system genuinely catches the entire class of bugs that haunts JavaScript.
The market is small. Elm’s pace of development slowed, PureScript stayed academic, OCaml’s web frontend story is mostly Bonsai-shaped. But for a language like the one I’m building, this is the closest thing to a natural home. If I do nothing fancy and just port Bonsai’s design to my language, that’s already a coherent, justifiable frontend story.
Five axes of optimization
The reason there are twelve paradigms and no winner is that they’re optimizing different things.
- Runtime speed: signals (Solid, Leptos), compile-time (Svelte). These win benchmarks because they do less work per state change.
- Bundle size: compile-time (Svelte), server-driven (HTMX). These win because they ship less code.
- Time-to-interactive: resumability (Qwik), islands (Astro), server-driven. These win because the client doesn’t have to execute much before being usable.
- Developer experience: VDOM (React), functional UI (Elm). React wins on raw ecosystem size and hireability; Elm wins on internal coherence and bug count.
- Type safety / correctness: functional UI (Elm, Bonsai), FRP, state machines. These win because the impossible states are gone before they ship.
A framework is good if it nails one of these axes. It’s great if it nails two. Nothing nails all five — the goals fight each other.
What’s actually moving in 2026
Rising: signals. Across languages, across frameworks. Angular adopted them. Vue is already there. Svelte 5 retreated from pure compile-time toward signals. Preact has them. Even React Compiler is, in effect, trying to fake them for code that wasn’t written with them in mind. If you’re picking a default reactivity model for a new project in 2026, signals are the obvious answer.
Rising: server-driven UI as a serious choice, not a retro one. HTMX has real adopters now. LiveView in Phoenix is mature. Inertia has bridged “Laravel-shaped backends” to “React-shaped frontends.” A lot of teams are admitting that the SPA model was overkill for their actual product.
Rising, slowly: islands and resumability. Both are limited by tooling maturity, but the underlying instinct — “the client doesn’t need to be the whole app” — is broadly correct.
Flat or declining: pure VDOM. React itself is moving away from the original VDOM-only story; its current direction is Server Components + Compiler, which is closer to “compile-time + server-driven” than to its 2013 self.
Niche but stable: FRP, functional UI, state machines. These are where they’ve been for a decade — small communities, strong internal logic, no breakout moment.
What this means for my language
I’m building an ML-flavored language with effect handlers, capability passing, region-based memory, and a small surface area. The question is which of these twelve paradigms can I credibly serve, and which one should I build for first.
The paradigms that line up best with what the language can already do:
- Fine-grained reactivity (signals) — capabilities are signals. Reading a signal inside a function is exactly the kind of effect my type system already tracks. This is the strongest mechanical fit.
- Functional UI (Elm / Bonsai) — the language is already ML-flavored. A Bonsai-style architecture would feel like writing OCaml in my language. Cheap to build, cheap to teach.
- State machines — sum types and pattern matching make state machines drop out for free. I don’t need to build “an XState” — the language already has the primitives.
- Compile-time reactivity (Svelte-style) — I own the compiler. This is more work, but it’s the most “language-specific” angle.
The paradigms that are weak fits:
- VDOM — generates garbage, fights my region allocator, doesn’t use any of my type system’s strengths. Not worth chasing.
- Pure Canvas/WebGL UI — viable but it’s a different product, not a frontend framework.
- Resumability — possible in principle, but the implementation cost outruns the win for now.
What I think I’ll actually do, in order:
- WASM target gets stable enough to render text in a browser. No framework.
- Imperative + helper layer — direct DOM access through
wasm-bindgen-equivalent bindings. Counter app works. - Introduce signals. This is the first real design decision. Capability passing maps onto signal subscription. I expect this to feel like Solid, but more typed.
- Declarative DOM construction — probably a tagged-template style closer to Lit than JSX. I want compile-time template parsing.
- Component composition. Here I’ll cross-check Bonsai’s design and decide whether to follow it closely.
- The boring necessary stuff: routing, forms, fetch, build tooling.
This is roughly six months to two years of side-project work. The early steps are small enough to ship as posts. The later steps depend on whether the language gets to a state where it’s worth using for real applications, which is a separate, harder question.
Closing
If you’re reading this because you’re picking a frontend stack today, the short version is: signals are the default now, server-driven is the right answer more often than people think, and pure VDOM is no longer the smart bet.
If you’re reading this because you’re building your own language and wondering whether to bother with a frontend story — yes, but build the language right first, and choose a paradigm whose primitives line up with primitives the language already has. There’s no point porting React if your language was built for something else.
I’ll write up the actual implementation steps as separate notes as the work progresses.