Web Development

Astro islands in practice: which components actually need hydration

DEV

Astro's islands architecture, ship static HTML by default and only hydrate the specific components that need interactivity, sounds simple in theory. In practice it forces a discipline most component libraries never asked us for: deciding, component by component, whether client-side JavaScript is actually necessary at all. This year, with the pattern well established and several client projects built on it, we went back through a handful of sites with a genuinely critical eye to see which of our hydration decisions actually held up.

Auditing components we assumed needed JavaScript

A surprising share of components we'd historically shipped as fully interactive turned out to need nothing beyond CSS hover states and a native `<details>` element once we actually looked closely. A pricing table with an "expand for details" toggle, built as a React component with local state in a previous project, became a plain `<details>` element with a few lines of CSS and zero client-side JavaScript. A tab interface that switched visible panels turned out to work fine with the `:target` pseudo-class and anchor links for the cases where full page navigation wasn't a UX problem.

  • Going component by component and asking "does this need to react to something the server can't know about" turned out to be a more useful filter than "does this look interactive," which is the question we'd been implicitly asking before.
  • The exercise found real JavaScript to cut on sites we'd already shipped, not just new ones, which suggested the discipline was worth applying retroactively, not only as a design-time habit for greenfield work.

Choosing the right hydration directive

Astro's hydration directives, `client:load`, `client:idle`, `client:visible`, `client:media`, and `client:only`, each answer a slightly different question, and picking the wrong one is an easy way to undercut the whole architecture. `client:load` hydrates immediately and should be reserved for anything above the fold that needs to respond to input right away, a header search box, for instance. `client:visible`, which hydrates a component only once it scrolls into view, gave us an easy performance win on long marketing pages with interactive elements far down the page, a comparison slider near the bottom of a product page, for example, that previously shipped its JavaScript on every page load regardless of whether a visitor ever scrolled that far.

  • `client:idle` proved useful for lower-priority interactive widgets, a newsletter signup form in a footer, where hydrating during the browser's idle time meant it was never competing with more important above-the-fold work for the main thread.
  • `client:media` let us conditionally hydrate a mobile-only navigation drawer without shipping its JavaScript to desktop visitors at all, since the directive ties hydration to a CSS media query rather than viewport visibility.

Where the model has limits

The islands model isn't free of tradeoffs, and we ran into a couple worth naming honestly. Shared state across islands doesn't come for free the way it does in a single-page application; two separate interactive components on the same page that need to communicate, a filter control and a results list, for example, require an explicit shared store or event-based communication pattern, since each island hydrates independently. That's not a dealbreaker, but it's an architectural decision you have to make deliberately rather than getting for free from component composition the way you would in React or Vue.

  • Debugging hydration mismatches, where server-rendered markup doesn't quite match what the client expects to hydrate onto, is a genuinely new class of bug for developers coming from fully client-rendered frameworks, and the error messages don't always point clearly at the cause.
  • Content that changes based on client-only information, viewport size, logged-in state read from local storage, needs `client:only` or a loading-state pattern, since Astro's static-first rendering has no way to know that information at build or server-render time.

What this changes about how we scope projects

The islands model is less about Astro specifically and more about a mindset every framework could benefit from: treat client-side JavaScript as a cost to justify per component, not a default to assume. We've started applying the same "does this need to react to the client" question during the design and scoping phase of projects, not just implementation, since it changes which components get flagged as needing custom interaction design in the first place. For content-heavy sites with pockets of real interactivity, the pattern has earned a permanent place in how we plan a build, not just how we write the code once the plan is set.

Measuring the actual JavaScript savings

We ran a before-and-after comparison on one client's marketing site rebuild, tracking total client-side JavaScript shipped per page across the site's most-visited templates. The homepage, previously a fully hydrated React page from an earlier iteration of the site, dropped from several hundred kilobytes of JavaScript to a small fraction of that once rebuilt on Astro with islands limited to an actual interactive product carousel and a contact form. Lighthouse performance scores moved accordingly, and the improvement showed up in real field data too, not just synthetic lab scores, once we compared Core Web Vitals reports from before and after the migration.

  • Time-to-interactive improved most dramatically on pages that had previously been over-hydrated relative to their actual interactivity, which tracks with the core thesis of the islands model: most of the win comes from not shipping JavaScript nobody needed, not from any particular runtime optimization.
  • Pages that were already interactivity-light before the rebuild, a simple static blog listing, for instance, showed a smaller but still real improvement, mostly from Astro's leaner baseline runtime compared to a full client-side framework's hydration bootstrap, even with minimal islands in use.

Working with a mixed-framework team

One underappreciated strength of the islands architecture showed up in team composition rather than performance metrics. Astro's per-component framework flexibility meant a team with both React and Vue experience could contribute islands in whichever framework they were more comfortable with, without forcing a single framework choice across the whole project. We used this deliberately on one project, keeping a couple of existing Vue components from a prior iteration of the client's site as Vue islands while building new interactive pieces in React, and it worked without the friction we expected going in.

  • Shared build tooling and a single deployment pipeline made the mixed-framework approach far less operationally messy than it sounds on paper; from a deployment and hosting perspective, it's one Astro site, regardless of how many component frameworks are contributing islands underneath.
  • The main friction was less technical and more a documentation and onboarding question: a new developer joining the project needs to know which framework a given island uses before touching it, which we solved with a simple naming convention and a short internal note rather than any tooling change.

Rolling this out beyond one project

Given how well this went, we've started defaulting new content-heavy client sites to Astro with islands rather than treating it as a special-case recommendation, and we've built a short internal checklist for the component-by-component interactivity audit so it happens consistently across projects rather than depending on one developer remembering to ask the right question every time.

Content editor experience

A less technical but genuinely important consideration turned out to be how the islands model interacts with the client's own content editing workflow. Editors adding new sections to a page through the CMS needed clear guidance on which content blocks were safe to combine freely, since a heavily interactive island embedded inside an otherwise static content area behaves differently for load performance depending on where it sits on the page. We ended up documenting a short "safe patterns" guide for the client's own content team, distinguishing blocks that are always static from ones that carry a small hydration cost, so that non-technical editors could make reasonably informed decisions without needing to understand the underlying architecture in any depth.

Revisiting older static-site assumptions

Astro's model also prompted us to revisit a few assumptions left over from earlier, pre-islands static site generators the team had used previously, particularly around how much logic is safe to push into build-time versus request-time rendering. A few pages we'd have defaulted to fully static in an older toolchain turned out to benefit from Astro's server-rendering option instead, since the underlying data changed frequently enough that a purely static build would have meant redeploying on every content update rather than serving fresh data on each request.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients