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:
┌────────────────────────────────────────────────────────────────┐│ 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:
- HTML arrives first (fast, SEO-friendly)
- JavaScript loads (takes time - network + parse + compile)
- 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:
┌─────────────────────────────────────────────────────────────────┐│ 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:
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:
┌─────────────────────────────────────────────────────────────────┐│ 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
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 nodeThis works, but it’s heavy. The whole component re-runs just to change one number.
Solid.js Model: Fine-Grained Reactivity
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-runsSolid.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:
┌────────────────────────────────────────────────────────────────┐│ 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
// 1. Server renders this HTML:// <button>Count: 0</button>
// 2. Client calls hydrateRoothydrateRoot(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
// 1. Server renders this HTML with markers:// <button data-hk="0">Count: 0</button>
// 2. Client calls hydratehydrate(() => <Counter />, document.getElementById('root'));
// 3. Solid finds the markerconst button = document.querySelector('[data-hk="0"]');
// 4. Create signal with initial value 0const [count, setCount] = createSignal(0);
// 5. Find text node and link to signalconst textNode = button.childNodes[1]; // "Count: " is 0, "0" is index 1createEffect(() => { textNode.textContent = count();});
// 6. Attach onClick handlerbutton.addEventListener('click', () => setCount(count() + 1));
// DONE! No VDOM, no diffing, no mismatch errors possibleThe 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:
// 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:
- Cross-platform: Same component model for web, native, canvas
- Ecosystem: Massive library support, mature tooling
- DevTools: Excellent debugging experience
- Predictable: Component re-renders are easier to reason about for some developers
Solid.js trades some of this for raw performance:
| Aspect | React (VDOM) | Solid.js (Fine-Grained) |
|---|---|---|
| Hydration | Re-run components + diff | Find markers + attach |
| Updates | VDOM diff then DOM update | Direct DOM update |
| Memory | VDOM tree during render | Minimal (just signals) |
| Debugging | Easy with DevTools | Requires understanding signals |
| Learning | Familiar mental model | Different mental model |
When Hydration Complexity Matters
I started to wonder: when does this actually matter?
It Matters for Large Apps
┌────────────────────────────────────────────────────────────────┐│ 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:
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
- All SSR frameworks need hydration - The interactivity gap is unavoidable
- VDOM makes hydration expensive - React must re-run components and diff
- Fine-grained reactivity makes hydration cheap - Solid.js just attaches handlers
- The question isn’t “hydration or not” - It’s “how complex is your hydration?”
- 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:
- 👨💻 Solid.js Server-Side Rendering Guide
- 👨💻 Solid.js Signals Tutorial
- 👨💻 hydrateRoot - React Documentation
- 👨💻 Reddit Discussion on Solid.js Hydration
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments