Fine-grained reactivity has become the defining technical conversation in frontend development. For over a decade, React's virtual DOM was the mental model everyone copied. In 2026, that's changed: SolidJS proved that signals with surgical DOM updates outperform React's diffing by a meaningful margin, and Svelte 5 adopted runes to bring similar fine-grained updates to its compiler model. React's response — the React Compiler (shipping with React 19) — is an automatic memoization pass that closes some of the gap without changing the programming model.
These three frameworks represent three distinct theories about where reactivity should live: in runtime primitives (SolidJS), in a compiler (Svelte), or in an optimizing compiler layered over hooks (React).
TL;DR
SolidJS has the most efficient reactivity model: signals are the finest possible granularity, updates are surgical, and there's no component re-rendering at all — only the specific DOM nodes that depend on a signal update. Svelte 5 achieves similar granularity with its rune system at the compiler level, with better ecosystem maturity and a gentler DX curve. React with the React Compiler closes the performance gap for typical applications — but still has a conceptual overhead (component re-renders, dependency arrays) that the signal-based frameworks eliminate. For performance-critical UIs, SolidJS is the leader. For practical day-to-day development with a team, Svelte 5 or React (with Compiler) are the pragmatic choices.
Key Takeaways
- SolidJS signals: zero VDOM, no re-renders — only the exact DOM nodes that depend on a changed signal update
- Svelte 5 runes (
$state,$derived,$effect): compiler-enforced reactivity, equivalent granularity to SolidJS with a more ergonomic API - React Compiler (React 19): automatic memoization that eliminates manual
useMemo/useCallbackbut still re-renders components (just fewer) - js-framework-benchmark: SolidJS and Svelte 5 consistently top the charts; React with Compiler meaningfully improves but doesn't fully close the gap
- Bundle size: Svelte 5 compiles to smaller bundles (no runtime); SolidJS runtime is ~7KB; React runtime is ~45KB
- Adoption: React ~190M downloads/week vs Svelte ~2.7M vs SolidJS ~1.5M — ecosystem matters
- The gap shrinks at app scale: at typical CRUD app complexity, users can't feel the difference
At a Glance
| SolidJS | Svelte 5 | React 19 (+ Compiler) | |
|---|---|---|---|
| Reactivity model | Runtime signals | Compiler + runtime runes | Hooks + optional compiler |
| Re-renders | None (component = factory) | None (compiled to DOM ops) | Component re-renders (fewer with Compiler) |
| Granularity | Signal-level (finest) | Rune-level (equivalent) | Hook call level (coarser) |
| Bundle size (min+gz) | ~7KB | ~5KB (no runtime) | ~45KB |
| npm downloads/week | ~1.5M | ~2.7M | ~190M |
| GitHub stars | ~35K | ~86K | ~230K |
| Meta-framework | SolidStart | SvelteKit | Next.js, Remix, Tanstack Start |
| JSX | ✅ (compile-time transformed) | ❌ (.svelte templates) | ✅ |
| TypeScript | ✅ | ✅ (.svelte with <script lang="ts">) | ✅ |
| React Compiler | N/A | N/A | ✅ (React 19) |
| SSR | ✅ SolidStart | ✅ SvelteKit | ✅ Next.js |
How Reactivity Works: The Core Differences
SolidJS: Runtime Signals
SolidJS components are factory functions — they run once to set up subscriptions, then never run again. Updates happen at the signal level, directly mutating DOM nodes that are subscribed to that signal.
import { createSignal, createEffect, createMemo } from 'solid-js'
// Component runs ONCE — not on every update
function Counter() {
const [count, setCount] = createSignal(0) // signal: reactive primitive
// createMemo: derived computation, updates only when count() changes
const doubled = createMemo(() => count() * 2)
// createEffect: runs when dependencies change
createEffect(() => {
console.log(`Count changed to ${count()}`)
})
// The JSX compiles to direct DOM operations, not VDOM
return (
<div>
{/* Only this text node updates when count() changes — nothing else */}
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
)
}
The critical difference: count is a function (count()) not a value. The getter creates a subscription — when this JSX expression is evaluated in a reactive context (during the initial render), SolidJS records "this DOM node reads count." When setCount() is called, only those specific DOM nodes update.
Svelte 5: Runes
Svelte 5 replaced its let/$: reactive syntax with explicit runes — compiler-recognized functions that signal reactivity intent:
<script lang="ts">
// $state: reactive state declaration
let count = $state(0)
// $derived: computed value (auto-tracks dependencies)
let doubled = $derived(count * 2)
// $effect: side effects (auto-tracks, reruns when deps change)
$effect(() => {
console.log(`Count changed to ${count}`)
})
</script>
<!-- Template compiles to direct DOM operations -->
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onclick={() => count++}>Increment</button>
Unlike SolidJS, count is a regular value (not a function call). The Svelte compiler statically analyzes which assignments can cause updates and generates the imperative DOM code. The result is equivalent granularity to SolidJS signals, but with a more natural syntax — you write count++ not setCount(c => c + 1).
React 19 with the React Compiler
React's model is fundamentally component-centric: state changes trigger component re-renders, and the reconciler diffs the new virtual DOM against the previous one.
import { useState, useMemo, useEffect } from 'react'
// 'use client' or just a component — re-renders when count changes
function Counter() {
const [count, setCount] = useState(0)
// Without Compiler: must manually memoize
const doubled = useMemo(() => count * 2, [count])
useEffect(() => {
console.log(`Count changed to ${count}`)
}, [count])
// ENTIRE component re-renders on every setCount()
// React Compiler: automatically memoizes stable sub-trees
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
)
}
The React Compiler (shipping with React 19) performs a Babel transform pass that automatically adds memoization to stable expressions — eliminating the need for manual useMemo, useCallback, and React.memo. But it doesn't eliminate component re-renders; it just ensures that stable sub-trees are memoized and skip reconciliation.
Performance: When the Difference Is Real
js-framework-benchmark Results (2025-2026)
The benchmark measures DOM operations: create 1K rows, select/swap/remove rows, create 10K rows.
| Framework | Geometric Mean Score | vs React baseline |
|---|---|---|
| Vanilla JS | 1.00 | baseline |
| SolidJS | ~1.08 | 2x faster than React |
| Svelte 5 | ~1.10 | 1.9x faster than React |
| Vue 3 (signals) | ~1.30 | 1.6x faster than React |
| React 19 (no compiler) | ~2.05 | baseline |
| React 19 (with Compiler) | ~1.70 | ~1.2x faster than base React |
Lower is better (multiplier of vanilla JS baseline). React with Compiler is a real improvement but SolidJS and Svelte 5 remain ~50-60% faster on heavy DOM manipulation workloads.
When the Benchmark Numbers Don't Matter
The benchmark measures extreme DOM-heavy scenarios. In most production apps:
Component tree: ~50-200 components
Update frequency: ~10-100ms between state changes
User-perceptible: differences < 16ms (one frame) are invisible
At this scale, React + Compiler, SolidJS, and Svelte 5 all feel instant.
The performance difference matters for:
- Data grids with 10,000+ rows
- Real-time dashboards updating > 30fps
- Complex animations with many tracked values
- Mobile devices with limited JS thread budget
Reactivity Patterns Compared
Derived State
// SolidJS
const total = createMemo(() => items().reduce((sum, item) => sum + item.price, 0))
// Re-computes only when items() changes
// Svelte 5
let total = $derived(items.reduce((sum, item) => sum + item.price, 0))
// Same — static analysis determines dependency
// React (with Compiler — no useMemo needed)
const total = items.reduce((sum, item) => sum + item.price, 0)
// Compiler automatically memoizes this if items is stable
Effects and Cleanup
// SolidJS
createEffect(on(userId, (id) => {
const subscription = subscribeToUser(id)
onCleanup(() => subscription.unsubscribe())
}))
// Svelte 5
$effect(() => {
const subscription = subscribeToUser(userId)
return () => subscription.unsubscribe() // cleanup function
})
// React
useEffect(() => {
const subscription = subscribeToUser(userId)
return () => subscription.unsubscribe()
}, [userId])
Stores / Global State
// SolidJS: createStore for nested object reactivity
import { createStore, produce } from 'solid-js/store'
const [store, setStore] = createStore({ user: { name: 'Alice', count: 0 } })
setStore('user', 'count', c => c + 1) // Only user.count DOM nodes update
// Svelte 5: $state works at any scope, including module scope
// store.svelte.ts:
export const userStore = $state({ name: 'Alice', count: 0 })
// Import and use directly — reactive everywhere
// React: Zustand, Jotai, or Context (not built-in fine-grained stores)
Bundle Size: The Compiler Advantage
Svelte's compile-to-vanilla-JS approach means apps ship with zero framework runtime:
React 19 runtime: ~45KB (min+gz)
SolidJS runtime: ~7KB (min+gz)
Svelte 5 runtime: ~5KB (min+gz) — mostly for SSR hydration
Svelte 5 (CSR-only): ~2-3KB (min+gz)
For large apps, the runtime cost is amortized over component code — but for small apps and first-party scripts, Svelte's approach is dramatically more efficient.
The Signals Convergence
Fine-grained reactivity is winning. In 2026, it's not just SolidJS and Svelte — Vue 3 ships signals via its Composition API, Angular 20 introduced native signals as a first-class primitive, and even React's Compiler is a pragmatic concession that the old "re-render everything" model needs help. The ecosystem has converged on the idea that tracking precise reactive dependencies is the right model.
This convergence matters for your framework choice: if you're evaluating SolidJS or Svelte 5 today, you're choosing a framework where the reactivity model is the primary design — not a feature bolted on. React is adapting. The others were built around it.
When to Choose Each
Choose SolidJS When:
- You need the absolute best DOM performance (real-time dashboards, games, data grids)
- Your team is comfortable with signals as the primary primitive
- You're building a new project and don't need React's ecosystem
- JSX is preferred over template syntax
- You want the most "pure" reactive system — no compiler magic, just runtime logic
Choose Svelte 5 When:
- You want fine-grained reactivity with ergonomic syntax (write
count++notsetCount(c => c + 1)) - Smallest possible bundle size matters
- Your team doesn't have a React background to migrate
- You're building content-heavy sites where SvelteKit shines
- You want a self-contained framework with routing (SvelteKit), stores, and transitions built in
Choose React (with Compiler) When:
- Your team already knows React — migration cost is real
- You need the largest ecosystem: component libraries, hiring pool, tutorials
- You're building on Next.js (App Router, PPR, Server Components)
- Enterprise environment where ecosystem stability matters more than peak performance
- You need React Server Components for zero-bundle server-rendered components
Migration Paths and Team Adoption Costs
Switching between these frameworks carries real cost that benchmark scores don't capture. A React team has years of institutional knowledge: component patterns, testing strategies, debugging muscle memory, and a library ecosystem where every UI package publishes a React version. Migrating to SolidJS or Svelte 5 requires rewriting components, replacing state management, and finding equivalents for every React-specific library in use. For most teams, the performance wins do not justify a full migration mid-project. The more realistic adoption path is to start new projects or isolated sub-applications in SolidJS or Svelte 5, letting teams build familiarity before committing. SolidJS's JSX syntax means React developers can read SolidJS code intuitively from day one; the conceptual shift — from re-renders to signals — is the real learning curve, not the syntax. Svelte 5's rune system requires learning a new set of primitives, but the compiler model means there is less runtime behavior to internalize once the syntax is familiar.
Community Ecosystem and Long-Term Stability
React's ecosystem advantage is difficult to overstate for production applications. Component libraries like shadcn/ui, Radix UI, Mantine, and Headless UI publish React-only packages. Data fetching patterns, testing utilities, and debugging tools are primarily documented for React. SolidJS's ecosystem is growing but remains small relative to React's decade of community investment. Svelte's ecosystem is larger than SolidJS's and growing faster, with SvelteKit driving adoption of component libraries that target Svelte specifically. For greenfield projects where ecosystem constraints are minimal — internal tools, data dashboards, performance-critical applications — SolidJS and Svelte 5 compete effectively with React. For applications that need to integrate broad third-party component libraries, React's ecosystem depth remains a practical advantage.
Compare SolidJS, Svelte, and React download trends on PkgPulse.
Related: SolidJS vs Svelte 2026 · React vs SolidJS 2026 · Next.js vs Astro vs SvelteKit 2026