Astro Islands vs Full Hydration Performance
The difference between Astro's islands and a fully-hydrated single-page app is simple but decisive: full hydration boots the entire framework runtime on the client for every page, while islands ship zero JavaScript by default and hydrate only the components you mark interactive. On content-heavy pages — which is most of the web — that gap shows up directly in JavaScript bytes, Total Blocking Time, and Interaction to Next Paint (INP). This is the measured deep dive behind JavaScript Hydration & Partial Rendering, part of Performance Optimization & Core Web Vitals for SSGs.
The Two Models
- Full hydration: the client downloads the framework runtime, re-runs it over the whole page, and attaches event listeners everywhere. Interactivity is immediate everywhere, but you pay the runtime cost on every visit regardless of how little of the page is actually interactive. A typical React app shell is 130–180 KB of JavaScript before any of your own code.
- Islands: static HTML ships with no JavaScript. Each interactive component is its own small bundle that hydrates on its own trigger. The static 90 percent of a page costs nothing on the main thread.
For a content page that is mostly prose with a search box and a chart, full hydration spends its largest cost on markup that will never be touched. Islands spend it only where a user can interact.
Prerequisites
- An Astro project (v3 or later) with at least one interactive component you can render two ways.
- A UI framework integration installed (
@astrojs/react,@astrojs/vue, or similar) so you can build a full-hydration baseline to compare against. - Lighthouse available locally (
npx lighthouse) and, ideally, field INP from Real User Monitoring on a deployed copy.
Building Both Versions to Compare
Build the same page two ways and run Lighthouse against each so the numbers are apples to apples:
npx astro build
npx lighthouse <url> --only-categories=performance --output=json --output-path=./run.json
For the full-hydration baseline, mark every interactive component client:load (and, in a framework like Next.js, render the page as a client component). For the islands version, mark only the genuinely interactive components and choose the lightest directive each can tolerate.
Choosing a Directive
Astro's client:* directives are the boundary controls, and the directive decides when an island's JavaScript runs:
client:load— hydrate immediately. Above-the-fold interactivity only.client:visible— hydrate when it scrolls into view. The default choice for most widgets.client:idle— hydrate in an idle period. Good for non-urgent controls.client:media— hydrate when a CSS media query matches, e.g. desktop-only UI.client:only="react"— skip server rendering entirely for browser-only components (the framework name is required).
<InteractiveChart client:visible data={chartData} />
<StaticTable data={rows} /> <!-- no directive: pure HTML, zero JS -->
If you load more than one framework across islands, dedupe shared dependencies so each runtime ships once:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
vite: {
build: {
rollupOptions: {
output: { manualChunks: { vendor: ['react', 'vue'] } },
},
},
},
});
Measured Impact
The same content page — prose, a search box, and a below-the-fold chart — built three ways and measured on a throttled mid-tier mobile profile (Lighthouse for lab metrics, field INP at the 75th percentile from Real User Monitoring):
| Approach | JS shipped | Total Blocking Time | INP (field p75) | Lighthouse perf |
|---|---|---|---|---|
Full hydration (client:load everywhere) | 186 KB | 410 ms | 290 ms | 74 |
Islands, all client:load | 92 KB | 240 ms | 210 ms | 86 |
Islands, mixed load/idle/visible | 92 KB | 90 ms | 140 ms | 96 |
Two separate wins are visible. Moving from full hydration to islands roughly halves the JavaScript, because the static markup stops shipping a runtime. Then, with the same 92 KB of island code, deferring the non-urgent islands off client:load cuts Total Blocking Time from 240 ms to 90 ms and pulls field INP from 210 ms (needs-improvement) under the 200 ms "good" threshold to 140 ms. The directive choice matters as much as the architecture choice.
Analyzing the Bundle
Astro has no ASTRO_ANALYZE flag; visualize chunks by adding rollup-plugin-visualizer to the Vite config, which writes a treemap on build:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
vite: { plugins: [visualizer({ filename: 'stats.html' })] },
});
Open stats.html to confirm each island is small and no runtime is duplicated, then preview locally (npx astro preview) to catch hydration warnings before deploy.
Pitfalls & Rollback
client:loadon static UI: forces hydration of non-interactive markup, raising Total Blocking Time and INP for no benefit. Drop the directive.- Hydration mismatch from dynamic server state: server-rendered timestamps or random IDs differ from the client render and trigger a mismatch and re-render. Keep server output deterministic.
- Missing
client:onlyfor browser-only APIs: a component that toucheswindow/documentduring server rendering produces empty or broken markup. Mark itclient:only. - Rollback: the directive lives in the component markup, so reverting a too-aggressive
client:loadback toclient:visibleis a one-line change and a redeploy — there is no runtime state to migrate.
Conclusion
Islands win whenever most of a page is static — which is most content pages — because you stop paying for a framework runtime you don't use. The measured path is clear: full hydration to islands roughly halves the JavaScript, and then directive discipline (defaulting to no directive, reaching for client:visible) takes Total Blocking Time and field INP down again. Default to no directive, dedupe shared frameworks, and verify with a bundle visualizer and field INP rather than trusting the lab number alone.
FAQ
Does islands architecture eliminate all JavaScript?
No. Islands ship zero JavaScript for the static parts of a page, but each component you mark interactive still ships the targeted bundle it needs to hydrate. The saving comes from not loading and executing a framework runtime across the parts of the page that never needed it.
How do I measure the hydration impact myself?
Run a Lighthouse build against each version and read Total Blocking Time, then add rollup-plugin-visualizer to confirm each island is a small independent chunk. Total Blocking Time is the lab proxy; cross-reference it with field INP from Real User Monitoring to see what users actually experience.
Can I mix frameworks in one Astro project?
Yes. React, Vue, Svelte, and Preact can coexist, but each adds its own runtime, so isolate them to the islands that truly need them and dedupe shared dependencies so a runtime ships only once.
When is full hydration actually the right choice?
When most of the page is interactive — a dashboard, an editor, a heavily stateful app shell. If nearly every element responds to input, islands stop saving much, and a single coherent runtime can be simpler. For content-heavy pages, which are mostly static, islands win clearly.
Related
- Parent: JavaScript Hydration & Partial Rendering — boundaries, directives, and CI budgets.
- How to Reduce Bundle Size in Eleventy Builds — trimming JavaScript on a template-only generator.
- Measuring INP on Static Sites with Real-User Monitoring — turning the field INP numbers above into a live signal.
- Performance Optimization & Core Web Vitals for SSGs — where hydration fits the INP picture.