Islands vs Full Hydration: Which Should a Content Site Choose?
This comparison sits under Hydration & Partial Rehydration Strategies in JavaScript Bundle Optimization & Code Splitting. Both architectures server-render the same HTML and both can produce a fast first paint. They differ in what happens next: full hydration re-creates the entire component tree on the client, islands re-create only the parts you marked. That single difference decides your interaction latency, your bundle size, and how hard cross-component state becomes.
Rapid Diagnosis: Measure Your Interactive Ratio
- Count interactive components as a share of total. Below ~20%, islands will win decisively. Above ~60%, the state-sharing cost usually outweighs the savings.
- Check whether interactivity is clustered or scattered. Three self-contained widgets suit islands. Fifty controls that all read the same store do not.
- Look at your current hydration task. If it is under
50mson a throttled profile, you have no problem to solve and a migration would be pure cost. - Ask how much shared client state exists. A theme toggle and a cart badge are cheap to externalise. A multi-step form spanning six components is not.
The Comparison
| Axis | Islands | Full hydration |
|---|---|---|
| JavaScript shipped | Only marked components | Whole tree plus framework runtime |
| Hydration cost | One short task per island | One long task for the tree |
| Interaction latency after load | Low — thread is mostly idle | Elevated until the tree settles |
| Shared client state | External store or events only | Native context and stores |
| Client-side routing | Usually absent or opt-in | Built in |
| Migration effort from an existing SSR app | High — component-by-component | None |
| Best fit | Docs, marketing, blogs, catalogues | Dashboards, editors, apps |
Root Cause Analysis: Where Each Model Actually Costs You
1. Islands break the shared-state assumption. Each island is a separate root. React context, framework stores and prop drilling stop at the island boundary, so a cart badge in the header and an add-to-cart button in the body cannot share a provider. You need an external store, custom events, or URL state — a real refactor in a mature codebase.
2. Full hydration pays for components that never change. A prose article of 300 static nodes is instantiated on the client for no benefit whatsoever. That is the cost islands delete, and it scales with page length, which is exactly why content sites feel it most.
3. Islands complicate client-side routing. Without a client router, every navigation is a document load. That is often fine — and with a modern view-transition or prefetch layer it can feel instant — but it changes how you think about preserving state across pages.
4. Full hydration hides its cost behind a fast paint. LCP looks healthy because the HTML arrived early, so teams routinely ship a page with a 1.3s LCP and a 500ms INP and believe they have a fast site. Only interaction measurements reveal the problem.
Step-by-Step Resolution: Making the Call
1. Measure the interactive ratio. Do this before any architectural debate.
// Rough census: how many rendered elements actually have behaviour?
const all = document.querySelectorAll('body *').length;
const interactive = document.querySelectorAll(
'button, a[href], input, select, textarea, [role="button"], [tabindex]'
).length;
console.log(`${interactive}/${all} = ${(interactive / all * 100).toFixed(1)}% interactive`);
// trade-off: this counts DOM elements, not components, so it over-counts links
// in prose. Treat it as an order-of-magnitude signal and confirm against your
// component tree before committing to a migration.
Expected outcome: a number that usually settles the argument — content sites typically land under 10%.
2. If islands look right, start with the heaviest route, not the whole site. A single high-traffic template proves the model and surfaces the state-sharing problems early.
// One route, three islands: everything else is HTML.
<ArticleLayout>
<TableOfContents client:idle /> {/* nice to have, not urgent */}
<article>{content}</article> {/* zero JS */}
<NewsletterForm client:visible /> {/* below the fold */}
<ThemeToggle client:load /> {/* must work immediately */}
</ArticleLayout>
// trade-off: three islands means three small bundles and three hydration tasks
// instead of one. Below roughly a dozen islands per page that is a clear win;
// past that, the per-island overhead starts to add up.
Expected outcome: hydration cost on that route drops by an order of magnitude while the rest of the site is untouched.
3. If full hydration must stay, get the same benefit with deferral. You do not need an architectural migration to stop hydrating off-screen components — see deferring below-the-fold hydration for the pattern, and selective hydration with Suspense for splitting what remains.
4. Externalise the state islands cannot share, once. A tiny signal store or a typed event bus is usually 30 lines and removes the main blocker.
// A minimal cross-island store: no framework, no provider tree.
const listeners = new Set();
let cart = JSON.parse(localStorage.getItem('cart') || '[]');
export const getCart = () => cart;
export function setCart(next) {
cart = next;
localStorage.setItem('cart', JSON.stringify(cart));
listeners.forEach((fn) => fn(cart));
}
export const subscribe = (fn) => (listeners.add(fn), () => listeners.delete(fn));
// trade-off: you are re-implementing a slice of what your framework gave you,
// including the SSR-safety rules. Keep the surface tiny — one store for the
// two or three values that genuinely cross islands, not a global state layer.
Expected outcome: islands can coordinate without a shared component tree, at the cost of a small hand-written module.
Verification
Compare the two architectures on the same route with the same content, on a throttled profile. The numbers that should move are total blocking time and the duration of the longest task after first paint; LCP should be roughly unchanged, because both models paint the same server HTML.
Then check the thing migrations most often break: interactivity that used to work. Walk the route with the keyboard, confirm every control still responds, and confirm state that used to be shared still is. A silently dead island is easy to ship because the markup still looks right.
In the field, look at INP for interactions in the first two seconds after navigation. That is where full hydration's cost concentrates and where an islands migration shows its biggest gain — often the difference between a 340ms and a 90ms p75 on mid-tier mobile.
FAQ
Can I use islands inside an existing React app?
Partially. You can mount independent roots for interactive widgets and leave the rest as server-rendered HTML, which is islands in everything but name. What you cannot easily keep is a single React context spanning them — each root is isolated. For most apps the pragmatic path is deferral and selective hydration first, and a genuine islands framework only when starting a new surface.
Do islands hurt SEO or accessibility?
No. Both models emit the same server-rendered HTML, and islands emit less JavaScript on top of it, so crawlers see identical content. The accessibility risk is the same in both: an interactive control that never hydrates is inert. Test with the keyboard after any change to hydration timing, because that is where a silent failure hides.
What about client-side routing — do I lose instant navigation?
You lose the framework's built-in router, but not necessarily the feel. Prefetching the next document on hover or viewport entry, plus view transitions, gets most content sites to navigation that is indistinguishable from a client router — without shipping one. Where you genuinely need preserved client state across navigations, that is a signal your site is closer to an app than a content site.
How long does an islands migration usually take?
Plan it per template rather than per site, because the effort is dominated by untangling shared state rather than by moving markup. A content template with two or three self-contained widgets is often a day; a template whose components all read from one context can take a week, most of it spent deciding what the external store should look like. Migrating the highest-traffic template first gives you the measurement and the state design before you commit to the rest.
Can the two architectures coexist in one codebase?
Yes, and that is usually how a migration actually happens. Content routes become islands while application routes keep full hydration, sharing components through a package that works under both. The friction to watch for is a shared component that quietly depends on a context: it works in the app and silently fails in an island. Make that dependency explicit — a prop or an external store — and the same component can serve both.
Does an islands framework help if my pages are already fast?
Then it is not worth the migration. The gains here are real but bounded: they come from deleting hydration work that a fast page does not have. If your longest task after paint is already under the long-task line and your field INP is comfortably inside budget, the architecture is not your constraint, and the effort belongs somewhere the measurements point to.
Related
- Hydration & Partial Rehydration Strategies — the cost model behind both architectures.
- Fixing long hydration tasks in Next.js App Router — getting island-like behaviour inside a full-hydration framework.
- Dynamic Imports & Route-Based Splitting — the chunking strategy that pairs with either model.