SolidStart Islands & Partial Hydration
Solid already pays one of the lowest hydration bills in the ecosystem, because it never builds a virtual DOM to reconcile. SolidStart’s islands mode takes that advantage further: instead of hydrating a whole route, it activates only the interactive regions you designate and leaves the rest of the page as inert server-rendered markup. For performance engineers, the combination is compelling — fine-grained reactivity means hydration cost scales with the number of reactive bindings rather than the size of the DOM, and islands mode means most of the DOM has no bindings at all. This page explains how Solid’s reactivity model and SolidStart’s islands mode interlock to deliver true partial hydration, and sits within the broader Framework-Specific Islands & Streaming SSR strategies for shipping less JavaScript.
Concept Definition & Scope
Two distinct ideas combine on this page, and it helps to separate them before wiring them together.
The first is Solid’s reactivity model. Solid compiles JSX to imperative DOM-creation code at build time. There is no virtual DOM and no component re-render: a createSignal produces a getter/setter pair, and reading that getter inside JSX creates a fine-grained subscription so that when the signal changes, Solid updates exactly the text node or attribute that depends on it — nothing else re-runs. On the server, the same components render to an HTML string. On the client, hydrate walks that server-rendered DOM and re-establishes those fine-grained subscriptions against the existing nodes. Because there is no tree to reconcile, hydration cost is a function of how many reactive bindings a component has, not how many DOM nodes it produced.
The second is SolidStart islands mode. By default a SolidStart route is server-rendered and then hydrated in full. Islands mode inverts that: page markup is static unless a component is designated an island, and only islands ship JavaScript and hydrate. This is the same hydration boundary discipline that Astro islands and client directives enforce with directives, but Solid’s fine-grained runtime means each island’s hydration is already unusually cheap.
The multiplicative effect is the point: islands mode shrinks how much of the page hydrates, and fine-grained reactivity shrinks how expensive each hydration is. The scope of this guide is exactly that intersection — enabling islands, choosing island entry points, using clientOnly for browser-only widgets, and reasoning about the no-vdom hydration cost. The precise configuration recipe lives in configuring SolidStart island hydration directives.
Technical Mechanics
Fine-grained reactivity and what hydration actually does
A Solid island is an ordinary Solid component. What makes hydration cheap is that its reactive bindings are the only live wires after the DOM is already present.
// ~/components/LikeButton.tsx
// An island component. createSignal returns a [getter, setter] pair.
import { createSignal } from "solid-js";
export default function LikeButton(props: { initial: number }) {
// On hydration, Solid does NOT re-run this to build a tree — it reuses the
// server-rendered DOM and binds `count` to the exact text node below.
const [count, setCount] = createSignal(props.initial);
return (
<button
// The click handler is the only listener attached for this island.
onClick={() => setCount((c) => c + 1)}
aria-label="Like"
>
{/* Only THIS text node is a reactive binding. Updating count() rewrites
just this node — no component re-render, no diff. */}
♥ {count()}
</button>
);
}
Derived state uses createMemo, and side effects use createEffect — both create their own fine-grained subscriptions rather than triggering component-level work:
// ~/components/Cart.tsx
import { createSignal, createMemo, createEffect } from "solid-js";
export default function Cart(props: { items: { price: number }[] }) {
const [items, setItems] = createSignal(props.items);
// createMemo caches a derived value; it re-computes ONLY when items() changes,
// and only its own dependents update — hydration wires this into the graph.
const total = createMemo(() =>
items().reduce((sum, i) => sum + i.price, 0)
);
// createEffect runs after hydration and re-runs only when total() changes.
// Use it for genuine side effects, not for rendering.
createEffect(() => {
document.title = `Cart · £${total().toFixed(2)}`;
});
return <output>Total: £{total().toFixed(2)}</output>;
}
clientOnly for components that must not server-render
Some components cannot render on the server: they read window, touch localStorage during render, or wrap a library that assumes a browser. Server-rendering them either crashes SSR or produces markup that differs from the client, causing a hydration mismatch. clientOnly skips SSR for that subtree and mounts it purely in the browser.
// ~/routes/dashboard.tsx
import { clientOnly } from "@solidjs/start";
// The map widget reads the DOM and window at construction time, so it must not
// run on the server. clientOnly defers the import and mount entirely to the
// client, rendering a fallback in its place during SSR.
const HeatMap = clientOnly(() => import("~/components/HeatMap"));
export default function Dashboard() {
return (
<main>
<h1>Usage</h1>
{/* Static content around the island still server-renders normally. */}
<HeatMap fallback={<div style="min-height:320px">Loading map…</div>} />
</main>
);
}
The trade-off is explicit: a clientOnly component contributes nothing to the initial HTML (so it cannot be your LCP element), but it also carries zero hydration mismatch risk because there is no server output to diverge from. For the exact configuration and island-entry conventions, see configuring SolidStart island hydration directives.
Passing state across the boundary
Islands receive their initial state as props, serialised at the server-client handoff. Keep those props plain and JSON-serialisable; passing functions or class instances across the boundary is the most common source of hydration failures, exactly as covered in cross-boundary prop passing. Solid serialises island props automatically, but only for values it can serialise.
SolidStart Islands vs Other Hydration Models
| Dimension | SolidStart islands (fine-grained) | Full-page Solid hydration | Virtual-DOM islands (React/Preact) |
|---|---|---|---|
| Hydration unit | Designated island components only | Entire route tree | Directive-marked components |
| Per-island cost | O(reactive bindings) — no diff | O(reactive bindings), whole page | O(nodes) — re-render + reconcile |
| Static markup JS cost | 0 KB | Runtime + all component code | 0 KB for unmarked components |
| Re-render model | None — signals update exact nodes | None — signals update exact nodes | Component re-render on state change |
| Browser-only widgets | clientOnly skips SSR cleanly |
clientOnly or isServer guards |
Dynamic import + ssr:false |
| Mismatch risk surface | Small — few islands, plain props | Whole tree must match | Whole marked subtree must match |
| Best fit | Content-heavy pages, few interactive zones | Highly interactive app-like routes | Existing React estates adopting islands |
The column that most often decides adoption is per-island cost. Because Solid never reconciles, moving from a virtual-DOM islands framework to SolidStart typically reduces the hydration overhead of each interactive zone even before islands mode removes zones entirely.
Step-by-Step Integration
- Enable islands mode in
app.config.ts. Turn on the experimental islands flag so SolidStart treats markup as static by default and only hydrates designated islands. The full configuration is detailed in the island hydration directives guide.
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
// ssr: true keeps server rendering on; islands mode then narrows hydration
// to designated island components instead of the whole route.
ssr: true,
experimental: { islands: true },
});
-
Keep route components static. Author your routes as plain server-rendered Solid components — headings, layout, copy. Do not introduce signals or effects at the route level; push them down.
-
Extract interactive UI into small island components. Each island should be a tight leaf: a like button, a cart summary, a filter control. Small islands keep each client chunk small and each hydration cheap.
-
Use
clientOnlyfor browser-only widgets. Anything that readswindowor wraps a DOM-only library goes throughclientOnlywith a sized fallback so SSR stays clean and layout is reserved. -
Serialise island props as plain data. Pass numbers, strings, and plain objects/arrays across the boundary. For anything richer, follow the serialisation patterns in cross-boundary prop passing.
-
Measure the split. Build for production and confirm that a mostly static route ships only the island chunks — no route-wide bundle. This is your guard against islands mode silently regressing to full hydration.
Where Islands End and the Router Begins
SolidStart sits in an unusual position: it can render a route as a fully client-navigated application, as a set of islands inside a server-rendered document, or as a mixture where some routes hydrate fully and others ship almost nothing. Choosing per route rather than per project is the point, and the choice hinges on what happens at navigation.
A fully client-navigated route keeps a router and a component graph resident in memory, so moving between two such routes costs a data fetch and a render — no document request, no re-parse. That is worth paying for in an application shell where visitors move constantly between views. An island route, by contrast, is a document: navigating to it is a fresh request, and its islands activate from scratch. That is worth paying for on entry pages, where the visitor arrives cold and most of them will read rather than navigate.
The mistake to avoid is mixing the two models within one navigation surface. If half your primary navigation performs a client-side transition and the other half triggers a document load, the interface feels inconsistent in a way visitors notice but cannot name — one link responds instantly, its neighbour flashes white. Draw the line at a boundary that matches how people actually move through the product: marketing and content routes as islands, the authenticated application as a navigated shell, and one deliberate document load at the seam between them.
Inside an island route, the server still owns data loading. Route-level loaders run on the server, their results are serialised into the document, and islands read those values as props rather than re-fetching on mount. Skipping that step is the most common cause of a route that looks fast in a synthetic test and feels slow in the hand: the markup arrives quickly, then every island issues its own request and the page fills in piecemeal several hundred milliseconds later.
Measurement & Validation
Confirm which components hydrated. A short effect that logs on mount tells you, per island, that hydration ran there and only there:
// Drop into an island temporarily to confirm it hydrated (and that a
// supposedly-static component did NOT log anything).
import { onMount } from "solid-js";
onMount(() => performance.mark(`island:hydrated:${props.id}`));
Then read the marks back with a PerformanceObserver, exactly as in the core measurement workflow. A static component that never logs is proof the island boundary held.
Confirm the payload in DevTools. In the Network panel, filter for scripts and verify a content-heavy route requests only your island chunks. In the Performance panel under a 4G / CPU 4× profile, the hydration tasks should be short and sparse — the absence of a wide reconciliation flame is the visible signature of no-vdom hydration.
Assert island props are serialisable. Add a test that renders a route to a string and parses the serialised island props out of the HTML. If serialisation throws, you are passing a non-serialisable value across the boundary and will get a runtime mismatch in the browser.
Failure Modes
1. State declared at the route level defeats islands mode. If a route component itself calls createSignal or createEffect, SolidStart must hydrate the route to keep that state live, pulling the whole tree back into hydration. The symptom is a route-wide client chunk where you expected only island chunks. Fix: move all reactivity into island leaf components and keep route/layout components purely presentational.
// WRONG — signal at route level forces route-wide hydration.
export default function Page() {
const [open, setOpen] = createSignal(false); // ← hydrates the whole route
return <Layout>…</Layout>;
}
// RIGHT — the signal lives inside a small island; the route stays static.
export default function Page() {
return <Layout><DisclosureIsland /></Layout>;
}
2. Hydration mismatch from server/client divergence. Rendering Date.now(), Math.random(), or locale-dependent formatting inside an island produces different HTML on server and client; Solid then discards the server DOM and re-creates the subtree, erasing the benefit and sometimes flashing content. Fix: make island render deterministic — compute the volatile value on the server, pass it as a prop, or move the widget to clientOnly so there is no server output to diverge from.
3. Non-serialisable props across the boundary. Passing a function, Map, Date, or class instance as an island prop fails serialisation and the island either throws on hydration or arrives with undefined props. Fix: pass plain JSON-serialisable data and reconstruct richer types inside the island, following cross-boundary prop passing.
Frequently Asked Questions
How is Solid hydration cheaper than React hydration?
Solid compiles JSX into direct DOM instructions rather than a virtual DOM tree. Hydration walks the existing server-rendered DOM, wires reactive signals to the exact text nodes and attributes they control, and stops — there is no tree reconciliation or diff. The cost is proportional to the number of reactive bindings, not the size of the DOM.
What does islands mode change in SolidStart?
With islands mode enabled, SolidStart treats page markup as static by default and only ships and hydrates the components you designate as islands. Instead of hydrating the whole route, the client runtime activates discrete interactive regions, so a mostly static page loads almost no JavaScript.
When should I use clientOnly in SolidStart?
Use clientOnly for components that depend on browser-only APIs or third-party libraries that cannot render on the server — maps, canvas widgets, or anything reading window/localStorage at render time. clientOnly skips SSR for that subtree and mounts it only in the browser, avoiding hydration mismatches.
Related
- Configuring SolidStart Island Hydration Directives — the exact
app.config.tsflags, island entry conventions, andclientOnlyrecipe. - Astro Islands and Client Directives — the directive-driven islands model, for comparison with SolidStart’s approach.
- Qwik Resumable Architecture — a runtime that removes hydration entirely, versus Solid’s cheap-but-real hydration.
- SvelteKit Component Islands — another compile-oriented framework’s take on island boundaries.
- Understanding Partial Hydration — the mechanism SolidStart islands mode applies at the route level.