SolidJS vs React: Fine-Grained Reactivity vs Virtual DOM
SolidJS skips the virtual DOM entirely. React doesn't. Here's what that actually means for your bundle size, runtime speed, and developer experience.
The Core Difference Nobody Explains Well
React has a virtual DOM. SolidJS doesn't. That one sentence sounds like a footnote, but it's the entire story — everything else flows from it.
React works by re-running your component functions on every state change, diffing the resulting virtual DOM tree against the previous one, and then applying the minimum necessary patches to the real DOM. It's clever. It's also expensive in ways that compound at scale. Every re-render means calling your function again, allocating new objects, running the diff algorithm. When you've got 200 components and a state update fires 60 times per second, you feel it.
SolidJS takes a completely different approach. It compiles your JSX into fine-grained reactive subscriptions at build time. When a signal changes, only the exact DOM nodes that depend on that signal update — no diffing, no re-running functions, no component tree traversal. The component function runs exactly once, sets up subscriptions, and then gets out of the way forever.
Honestly, the first time you internalize what that means, it feels almost too good. And in practice, it largely is that good — with a few sharp edges worth knowing before you commit.
How React's Virtual DOM Actually Works
React 18 introduced the concurrent renderer, which lets it pause, interrupt, and prioritize renders. That's genuinely impressive engineering. But the fundamental model hasn't changed since 2013: your UI is a function of state, and React calls that function repeatedly to figure out what changed.
When you write useState, you're telling React "this value affects the render output, so re-run the component when it changes." The component function re-executes top to bottom. Hooks fire in order. React builds a new fiber tree, diffs it against the previous one, and flushes the minimal DOM updates. The magic of useMemo and useCallback is entirely about skipping parts of this process — which tells you something about how much work the process involves by default.
// React — this function runs EVERY time count changes
function Counter() {
const [count, setCount] = useState(0);
console.log('render'); // fires on every update
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}That console.log fires on every single state change. In a simple counter that's fine. In a complex component with derived state, context consumers, and children, it's where performance problems start. Worth noting: React's compiler (released in React 19) auto-memoizes a lot of this, which genuinely helps. But the underlying model is still the same.
The virtual DOM also has a real cost in bundle size. React + ReactDOM clock in around 42kb minified+gzipped. For a UI library, that's your base tax before you write a single line of product code.
How SolidJS Fine-Grained Reactivity Works
SolidJS uses signals — primitive reactive values that track which computations depend on them. When a signal's value changes, only those exact computations re-run. No component re-execution, no virtual DOM, no diffing. The DOM updates happen surgically, at the point of use.
// SolidJS — this function runs ONCE
function Counter() {
const [count, setCount] = createSignal(0);
console.log('setup'); // fires exactly once
return (
<div>
<p>{count()}</p> {/* this reactive expression updates in place */}
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}Notice that count is a function call — count() — not a plain value. That's how Solid tracks dependencies: accessing the signal inside a reactive context (like JSX, createEffect, or createMemo) registers the subscription. Change the signal, and only those subscribers update. It's the same mental model as MobX observables or Vue's composition API, but Solid takes it all the way to the DOM layer.
The practical upshot: SolidJS ships around 7kb minified+gzipped. That's not a typo. Compared to React's 42kb, you're saving 35kb before your app code starts. For performance-sensitive contexts — landing pages, mobile-first SPAs, anything where Time to Interactive matters — that gap is significant.
Quick aside: Solid's JSX looks almost identical to React's. The mental model shift is surprisingly small if you already know React. The main gotcha is that you can't destructure props — const { name } = props breaks reactivity because it captures the value at call time rather than tracking the live getter.
Performance: Benchmarks and What They Mean
The js-framework-benchmark puts SolidJS consistently at the top or within a few percent of the top across almost every metric — create 1000 rows, update every 10th row, select row, swap rows. React with the compiler does reasonably well. React without it lags noticeably on update-heavy workloads.
Look, benchmarks measure toy workloads. Your real app has API calls, complex business logic, third-party integrations, and a team of developers who may or may not understand how reactivity works. Solid winning a benchmark doesn't automatically mean your app will be faster — especially if your bottleneck is network latency rather than render cycles.
That said, there are real categories where Solid's model wins clearly. Data tables that update frequently (think live dashboards, trading UIs), real-time collaborative tools, heavily animated UIs where DOM mutations need to be surgical — these are where the fine-grained approach pays dividends you can actually measure in user-facing metrics. If you're building a glassmorphism dashboard with live data feeds updating 500ms, you'll feel the difference.
// Solid: only the specific <td> with price updates
// React: re-renders the entire row (or more) by default
// The gap widens with table size and update frequencyReact's concurrent features — useTransition, useDeferredValue, Suspense — are largely answers to problems that Solid's model doesn't have in the first place. Whether that's a point in React's favour (powerful escape hatches) or against it (complexity to manage a problem Solid avoids) depends on your perspective.
Developer Experience and Ecosystem
React's ecosystem is enormous. In 2026, there are React-specific solutions for virtually everything: routing (React Router v7, TanStack Router), data fetching (TanStack Query, SWR), forms (React Hook Form), animation (Framer Motion), state management (Zustand, Jotai, Redux Toolkit). When you hit a problem in React, there's a library for it and a Stack Overflow answer from 2021.
SolidJS's ecosystem is smaller but surprisingly complete for most use cases. SolidStart is the full-stack meta-framework (analogous to Next.js). @solidjs/router handles routing. solid-query ports TanStack Query's API. Many Headless UI patterns translate directly. You won't be blocked on ecosystem gaps for typical web apps, but you will occasionally hit a library that's React-only and have to roll your own or find an alternative.
The learning curve difference is real but not massive. If you already know React, the main things to internalize are: signals are functions (call them to read), don't destructure props, createEffect instead of useEffect but with automatic dependency tracking (no array needed), and createMemo instead of useMemo. You can be productive in Solid within a day. The deeper reactive patterns — stores, context, resources — take longer to get fluent with.
One more thing — TypeScript support in both frameworks is solid (no pun intended). Solid's types are slightly trickier around signals and generic components, but nothing insurmountable. If you're building a component library you want to ship publicly, React's wider adoption still makes it the more practical choice for maximum compatibility.
When to Pick Which
Pick React when: your team already knows it, you need the largest possible hiring pool, you're integrating with a Next.js or Remix application, or you need a specific library that's React-only. Also pick React if you're building something where SSR, edge rendering, and server components matter — Next.js's ecosystem is still well ahead of SolidStart in maturity.
Pick SolidJS when: raw performance is a genuine product requirement (not a premature optimization), you're building something new from scratch with a small team, your UI involves high-frequency updates, or you want a smaller bundle baseline. It's also a great pick if you want to deeply understand reactivity — building in Solid forces you to think about data flow more explicitly than React does.
In practice, most teams at mid-sized companies will still choose React in 2026. Not because it's better at runtime performance — it isn't — but because the risk-adjusted cost of hiring, onboarding, and ecosystem compatibility points there. SolidJS is the better technical choice for a narrow but real set of use cases. Acknowledging that isn't a cop-out, it's just accurate.
If you're building UI components you want to showcase or prototype quickly, both frameworks can consume components from Empire UI — our glassmorphism components and other style primitives are CSS/HTML-first enough that adapting them to Solid takes maybe 15 minutes once you have the signal patterns down. The gradient generator outputs plain CSS that works in either framework without modification.
Code Patterns Side by Side
Here's the same derived state pattern in both frameworks — a common source of confusion when migrating between them:
// React — derived value with useMemo
function PriceDisplay({ basePrice, taxRate }) {
const total = useMemo(
() => basePrice * (1 + taxRate),
[basePrice, taxRate]
);
return <span>${total.toFixed(2)}</span>;
}
// Solid — createMemo with automatic tracking
function PriceDisplay(props) {
const total = createMemo(() => props.basePrice * (1 + props.taxRate));
return <span>${total().toFixed(2)}</span>;
}The Solid version auto-tracks props.basePrice and props.taxRate because they're accessed inside the createMemo. No dependency array to maintain, no forgetting to add a dep and getting a stale value. The tradeoff is that the implicit tracking can surprise you if you access a signal outside a reactive context without realising it.
// React — side effect with explicit deps
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // forget this and you get bugs
// Solid — effect with automatic tracking
createEffect(() => {
document.title = `Count: ${count()}`; // count() access registers subscription
});The dep array is one of React's most common sources of bugs. Solid's automatic tracking eliminates the category entirely, at the cost of making the tracking implicit. Whether that's a win depends on whether your team is more likely to make dep array mistakes or reactive tracking mistakes. For most teams coming from React: dep arrays are the known enemy, and Solid's approach feels like a relief.
FAQ
In most benchmarks, yes — especially for frequent DOM updates. The fine-grained reactivity means SolidJS skips the diffing step React requires on every state change. For typical CRUD apps, the difference may not be perceptible to users.
No. Next.js is React-specific. SolidJS has its own full-stack meta-framework called SolidStart, which covers SSR, file-based routing, and server functions.
Signals are getter functions so the reactive system can track access. When you call count() inside a reactive context, Solid registers that context as a subscriber. Plain property access can't provide that hook.
Yes. SolidJS 1.x has been stable since 2022, and version 2.0 shipped in 2025 with improved async primitives. Companies like NVIDIA have used it in production. The ecosystem is smaller than React's but covers most use cases.