SolidJS vs Svelte 5 vs React: Reactivity 2026
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
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
See the live comparison
View solidjs vs. svelte vs react on PkgPulse →