Skip to content

Do Non-VDOM Frameworks Like Solid.js Need Hydration?

I was researching React alternatives when I stumbled onto a question that seemed obvious at first: “Do frameworks without a Virtual DOM need hydration?”

My assumption was simple: React needs hydration because it has to rebuild its VDOM tree. So if a framework like Solid.js doesn’t use VDOM, shouldn’t it skip hydration entirely?

Turns out, I was wrong. Let me explain why.

The Short Answer

Yes, Solid.js needs hydration.

But here’s the key insight that changed my understanding:

“The funny thing is, frameworks without a VDOM like Solid still need to hydrate. But their hydration is much closer to just hooking up a click handler to some server rendered HTML.” - Reddit comment (score 19)

This made me realize I was asking the wrong question. The question isn’t “does your framework need hydration?” The real question is “how complex is your hydration?”

The Interactivity Gap: Why All SSR Frameworks Need Hydration

I had to step back and think about what SSR actually does:

SSR Timeline
┌────────────────────────────────────────────────────────────────┐
│ 1. SERVER sends HTML │
│ └──► User sees content (but buttons don't work yet) │
├────────────────────────────────────────────────────────────────┤
│ 2. BROWSER loads JavaScript │
│ └──► Content still visible, still not interactive │
├────────────────────────────────────────────────────────────────┤
│ 3. HYDRATION runs │
│ └──► NOW the page is interactive! │
└────────────────────────────────────────────────────────────────┘

This gap is unavoidable. Even if you inline your JavaScript, there’s still a brief moment before execution starts. The HTML arrives before the interactivity.

This isn’t about React or Solid or Vue. It’s about how the web works:

  1. HTML arrives first (fast, SEO-friendly)
  2. JavaScript loads (takes time - network + parse + compile)
  3. Hydration bridges the gap (attaches behavior to existing structure)

So now the question becomes: what happens during that hydration step?

React’s Hydration: The Expensive Way

Let me trace through what React actually does during hydration:

React Hydration Process
┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: Re-execute ALL components │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ App() │ │
│ │ └── Header() │ │
│ │ └── Main() │ │
│ │ └── Sidebar() │ │
│ │ └── Content() │ │
│ │ └── Card() x 10 │ │
│ │ Each component runs AGAIN on client │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ STEP 2: Build VDOM tree │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ type: 'div', │ │
│ │ props: { className: 'app' }, │ │
│ │ children: [ ... nested VDOM nodes ... ] │ │
│ │ } │ │
│ │ Memory allocated for entire tree │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ STEP 3: Walk existing DOM │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ document.querySelectorAll('*') │ │
│ │ Compare VDOM nodes with DOM nodes │ │
│ │ Check: same tag? same attributes? same children? │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ STEP 4: Attach event handlers │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Finally: onClick, onChange, onSubmit, etc. │ │
│ │ Wire up state management │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

That’s a lot of work! And here’s where things can go wrong:

hydration-mismatch.jsx
function TimeDisplay() {
// Server time and client time are different!
return <span>{new Date().toLocaleString()}</span>;
}
// React walks the DOM, finds this span, checks the text content
// Server rendered: "3/30/2026, 10:00:00 AM"
// Client rendered: "3/30/2026, 10:00:05 AM"
// HYDRATION MISMATCH ERROR!

The problem is that React must recreate the exact state that produced the server HTML. Any difference causes an error.

Solid.js Hydration: The Simple Way

Now let me show you what Solid.js does:

Solid.js Hydration Process
┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: DOM is already there from SSR │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Server rendered HTML with special markers: │ │
│ │ <button data-hk="0-0">Count: 0</button> │ │
│ │ ↑ │ │
│ │ Compile-time marker │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ STEP 2: Find the markers │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ querySelectorAll('[data-hk]') │ │
│ │ No need to re-run components! │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ STEP 3: Attach computations │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Wire up signals to DOM nodes │ │
│ │ Add event listeners │ │
│ │ DONE! │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Notice what’s missing? No component re-execution. No VDOM tree. No diffing.

Why Is Solid.js So Much Simpler?

I had to dig into the architecture to understand this. The difference comes down to how each framework handles updates:

React’s Model: Re-render Everything

react-counter.jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
// When setCount is called:
// 1. Counter() runs AGAIN
// 2. New VDOM created
// 3. Diff old VDOM vs new VDOM
// 4. Update only the changed text node

This works, but it’s heavy. The whole component re-runs just to change one number.

Solid.js Model: Fine-Grained Reactivity

solid-counter.jsx
function Counter() {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount(count() + 1)}>
Count: {count()}
</button>
);
}
// When setCount is called:
// 1. Signal notifies subscribers
// 2. ONLY the text node updates
// 3. Component NEVER re-runs

Solid.js compiles JSX to direct DOM operations. The {count()} expression becomes a direct subscription to that specific text node. When the signal changes, only that text updates.

This difference carries over to hydration:

Hydration Comparison
┌────────────────────────────────────────────────────────────────┐
│ REACT │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Client must rebuild VDOM to match server HTML │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Run all │ ──►│ Build VDOM │ ──►│ Compare │ │ │
│ │ │ components │ │ tree │ │ with DOM │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │ │
│ │ Mismatch? ERROR! │ │ │
│ └──────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ SOLID.JS │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ DOM is already correct, just attach behavior │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Find │ ──►│ Attach │ DONE! │ │
│ │ │ markers │ │ handlers │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ No mismatch possible - we don't re-render! │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

What Actually Happens: A Closer Look

Let me trace through both approaches with the same component.

React’s Hydration

React hydration (simplified internal process)
// 1. Server renders this HTML:
// <button>Count: 0</button>
// 2. Client calls hydrateRoot
hydrateRoot(document.getElementById('root'), <Counter />);
// 3. React runs Counter() on client
// Creates: { type: 'button', props: { onClick: fn }, children: ['Count: ', 0] }
// 4. React finds the existing button in DOM
// 5. React checks: does VDOM match DOM?
// VDOM expects: button with text "Count: 0"
// DOM has: button with text "Count: 0"
// MATCH!
// 6. Attach onClick handler to existing button
// 7. Initialize useState with value 0
// BUT if step 5 doesn't match... HYDRATION ERROR!

Solid.js Hydration

Solid.js hydration (simplified internal process)
// 1. Server renders this HTML with markers:
// <button data-hk="0">Count: 0</button>
// 2. Client calls hydrate
hydrate(() => <Counter />, document.getElementById('root'));
// 3. Solid finds the marker
const button = document.querySelector('[data-hk="0"]');
// 4. Create signal with initial value 0
const [count, setCount] = createSignal(0);
// 5. Find text node and link to signal
const textNode = button.childNodes[1]; // "Count: " is 0, "0" is index 1
createEffect(() => {
textNode.textContent = count();
});
// 6. Attach onClick handler
button.addEventListener('click', () => setCount(count() + 1));
// DONE! No VDOM, no diffing, no mismatch errors possible

The key insight: Solid.js already knows where everything goes because it compiled the JSX ahead of time. React has to figure it out at runtime.

The Compile-Time Advantage

I realized the real difference happens before hydration even starts. Solid.js compiles JSX differently:

Solid.js JSX compilation
// You write:
<button onClick={() => setCount(count() + 1)}>
Count: {count()}
</button>
// Solid compiles to (simplified):
const template = document.createElement('button');
template.appendChild(document.createTextNode('Count: '));
template.appendChild(document.createTextNode('')); // placeholder for signal
function Counter() {
const [count, setCount] = createSignal(0);
const button = template.cloneNode(true);
// Direct DOM binding!
createEffect(() => button.childNodes[1].textContent = count());
button.addEventListener('click', () => setCount(count() + 1));
return button;
}

The template is created once. The effect is a direct subscription. No intermediate representation.

Why React’s Approach Is Still Valuable

To be fair, React’s VDOM approach has benefits too:

  1. Cross-platform: Same component model for web, native, canvas
  2. Ecosystem: Massive library support, mature tooling
  3. DevTools: Excellent debugging experience
  4. Predictable: Component re-renders are easier to reason about for some developers

Solid.js trades some of this for raw performance:

AspectReact (VDOM)Solid.js (Fine-Grained)
HydrationRe-run components + diffFind markers + attach
UpdatesVDOM diff then DOM updateDirect DOM update
MemoryVDOM tree during renderMinimal (just signals)
DebuggingEasy with DevToolsRequires understanding signals
LearningFamiliar mental modelDifferent mental model

When Hydration Complexity Matters

I started to wonder: when does this actually matter?

It Matters for Large Apps

Hydration Time Comparison (Conceptual)
┌────────────────────────────────────────────────────────────────┐
│ 100 component app │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ React: 100 components run × 2 = 200 executions │ │
│ │ Solid: 0 re-executions, just attach handlers │ │
│ └─────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ 1000 component app │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ React: 1000 components run × 2 = 2000 executions │ │
│ │ Solid: Still 0 re-executions, just attach handlers │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

The gap grows with app size.

It Matters for Complex Components

Components with expensive computations suffer more under React’s hydration:

expensive-component.jsx
function ExpensiveList({ items }) {
// This computation runs TWICE in React hydration
// Once on server, once on client
const processed = items.map(item => expensiveTransform(item));
return processed.map(item => <ItemCard key={item.id} {...item} />);
}

In Solid.js, the computation runs once (on the server). The client just attaches behavior.

The Trade-offs

After digging into both approaches, I see the trade-offs more clearly:

React’s Hydration Trade-offs

Pros:

  • Consistent mental model (components always re-render)
  • Excellent DevTools and debugging
  • Mature ecosystem

Cons:

  • Components run twice (server + client)
  • Hydration mismatch errors are common
  • Large VDOM trees consume memory
  • Slower for complex pages

Solid.js Hydration Trade-offs

Pros:

  • No component re-execution
  • No hydration mismatches (DOM is already correct)
  • Faster for large pages
  • Lower memory usage

Cons:

  • Signals require different mental model
  • Smaller ecosystem
  • Debugging can be harder
  • Less cross-platform flexibility

Key Takeaways

  1. All SSR frameworks need hydration - The interactivity gap is unavoidable
  2. VDOM makes hydration expensive - React must re-run components and diff
  3. Fine-grained reactivity makes hydration cheap - Solid.js just attaches handlers
  4. The question isn’t “hydration or not” - It’s “how complex is your hydration?”
  5. Choose based on your needs - React for ecosystem and familiarity, Solid.js for performance

The Reddit insight was spot on: Solid.js’s hydration is “much closer to just hooking up a click handler to some server rendered HTML.” That simplicity has real performance benefits for large SSR applications.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments