Island Hydration Patterns

The Core Principle

Every island has a hydration strategy that controls when its JavaScript activates. The right choice depends on a single question: how soon does the user need this component to be interactive?

Pages without islands ship zero JavaScript. Each island you add is a deliberate decision to enhance a specific part of the page. The hydration strategy is how you control the cost of that decision.

The Four Strategies

StrategyActivatesBrowser APICost
loadImmediately on page loadDirect callBlocks initial rendering
idleWhen the browser has spare cyclesrequestIdleCallbackDeferred, low priority
visibleWhen scrolled into viewIntersectionObserverZero cost until visible
mediaWhen a CSS media query matchesmatchMediaConditional, may never fire

Choosing the Right Strategy

Use load for: Primary Interactive Components

Components where the user expects immediate interaction. Deferring these risks a broken user experience – the user clicks or types before the JavaScript is ready.

Examples:

  • Contact forms, login forms, checkout flows
  • Navigation menus with JavaScript-driven dropdowns
  • Dark mode toggles (visual flash if deferred)
  • Cookie consent banners (legal requirement to show immediately)

Why not always use load? It runs synchronously during page parse. Every load island adds latency before the page is fully interactive. Use it only when the component genuinely needs to be ready at first paint.

<accent-island data-component="contact-form" data-hydrate="load">
  <form action="/contact-submit" method="POST">
    <!-- Server-rendered form works without JS -->
    <input type="text" name="name" required>
    <button type="submit">Send</button>
  </form>
</accent-island>

The form works without JavaScript (standard POST). When JS loads, the island immediately intercepts submit for inline validation and fetch-based submission. Deferring to idle would risk the user submitting before the island hydrates, bypassing client-side validation.

Use idle for: Secondary Enhancements

Components that improve the experience but are not critical for immediate interaction. The user does not directly interact with these on first page load.

Examples:

  • Word count and reading time displays
  • Copy-to-clipboard buttons on code blocks
  • Analytics or tracking widgets
  • Social share buttons
  • Table of contents highlights

Why idle and not load? These components enhance content that is already readable. The user’s first action is to read, not to click the copy button. Waiting for browser idle cycles keeps the main thread free for rendering.

<accent-island data-component="copy-code" data-hydrate="idle">
  <pre><code>cargo run -- serve</code></pre>
</accent-island>

The code block is fully readable without JS. The copy button appears after hydration – a nice-to-have, not a must-have-immediately.

Use visible for: Below-the-Fold Content

Components that are not visible when the page first loads. There is no reason to hydrate something the user cannot see yet.

Examples:

  • Image galleries or carousels further down the page
  • Comment sections at the bottom of articles
  • Related content widgets
  • Search widgets in a sidebar that requires scrolling
  • Interactive charts in long-form content

Why visible and not idle? Even idle hydration runs soon after page load. For content below the fold, the user may never scroll to it. visible ensures zero JavaScript cost for content the user does not reach.

<accent-island
  data-component="chart"
  data-hydrate="visible"
  data-props='{"type": "bar", "endpoint": "/api/stats"}'>
  <!-- Static fallback: a plain data table -->
  <table>
    <tr><td>Q1</td><td>42,000</td></tr>
    <tr><td>Q2</td><td>45,000</td></tr>
  </table>
</accent-island>

The data table is useful without JS. When the user scrolls to it, the chart component enhances it into an interactive visualization.

Use media for: Device-Specific Components

Components that should only activate on certain screen sizes or device capabilities. If the media query does not match, the island never hydrates – zero cost.

Examples:

  • Mobile hamburger menus (desktop uses a static nav)
  • Touch-specific carousels (desktop shows all items)
  • Reduced-motion alternatives
<accent-island
  data-component="mobile-menu"
  data-hydrate="media"
  data-media="(max-width: 768px)">
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
    <a href="/contact">Contact</a>
  </nav>
</accent-island>

On desktop (wider than 768px), the static nav links render as-is. On mobile, the island hydrates and transforms them into a hamburger menu.

Decision Flowchart

Ask these questions in order:

  1. Does the user interact with this component immediately on page load? Yes -> load. (Forms, navigation, consent banners.)

  2. Is the component visible above the fold? No -> visible. (Don’t hydrate what the user can’t see.)

  3. Does the component only apply to certain screen sizes? Yes -> media. (Mobile menus, touch carousels.)

  4. Everything else -> idle. (Enhancements, secondary UI, analytics.)

Common Patterns

Pattern: Form Enhancement

Server-rendered HTML forms work without JavaScript. The island adds client-side validation, inline feedback, and fetch-based submission.

Strategy: load – the form is the primary page interaction.

<accent-island data-component="contact-form" data-hydrate="load">
  <form action="/contact-submit" method="POST" class="contact-form">
    <label>Email <input type="email" name="email" required></label>
    <button type="submit">Send</button>
  </form>
</accent-island>

Key insight: The required attribute and type="email" provide native browser validation as a baseline. The island’s JavaScript should use form.checkValidity() to leverage this, not replace it with weaker custom checks.

A search form that works as a standard GET request without JS, then enhances to client-side filtering with the island.

Strategy: visible or idle – search is secondary to reading.

<accent-island
  data-component="search"
  data-hydrate="visible"
  data-props='{"endpoint": "/_search/index.json"}'>
  <form action="/search" method="get">
    <input type="search" name="q" placeholder="Search...">
    <button type="submit">Search</button>
  </form>
</accent-island>

Pattern: Metadata Display

Passive display components that compute and show a value from page content.

Strategy: idle – informational only, no interaction expected.

<accent-island data-component="reading-time" data-hydrate="idle">
  <span class="reading-time">...</span>
</accent-island>

Pattern: Conditional Mobile Enhancement

Desktop shows static content; mobile gets an interactive version.

Strategy: media – never hydrates on desktop.

<accent-island
  data-component="swipe-gallery"
  data-hydrate="media"
  data-media="(max-width: 768px)"
  data-props='{"autoplay": false}'>
  <div class="gallery">
    <img src="/media/photo-1.jpg" alt="Photo 1">
    <img src="/media/photo-2.jpg" alt="Photo 2">
  </div>
</accent-island>

Performance Impact

Each hydration strategy has a different performance profile:

StrategyMain Thread ImpactNetwork ImpactWhen to Worry
loadImmediate (blocks TTI)Script fetched eagerlyMore than 2-3 load islands per page
idleDeferred (~200ms)Script fetched eagerlyMany idle islands competing for idle time
visibleNone until scrolledScript fetched eagerlyLarge islands with heavy init logic
mediaNone if query missesScript fetched eagerlyRarely a concern

Note: All island scripts are fetched when the page loads (via defer script tags). The hydration strategy controls when the init function runs, not when the script is downloaded. For pages with many islands, consider whether all scripts need to be in the initial HTML – per-page script loading via templates gives finer control.

Anti-Patterns

Using load for everything

Every island is load -> the page becomes JavaScript-heavy with no deferral. This defeats the purpose of the islands architecture. Reserve load for components that genuinely need immediate interactivity.

Using visible for above-the-fold content

If the component is already visible when the page renders, visible still works – the IntersectionObserver fires immediately. But it adds an unnecessary observer setup and one-frame delay. Use idle or load instead for above-the-fold components.

Forgetting the fallback

Every <accent-island> element should contain functional HTML that works without JavaScript. If the island fails to hydrate (script error, network failure, JS disabled), the user still gets a working page. An empty <accent-island> with no fallback content is a blank hole in the page.

Hydrating content that does not need JavaScript

Not everything needs to be an island. Static content (text, images, tables) that requires no interaction should stay as plain HTML. Islands are for adding behavior to content, not for rendering content itself.

Combining Strategies on a Page

A well-optimized page uses multiple strategies:

+------------------------------------------+
|  [load] Dark mode toggle   [load] Nav    |  <- Critical UI, immediate
+------------------------------------------+
|  Article title                           |
|  [idle] Reading time: 5 min              |  <- Enhancement, deferred
|                                          |
|  Article body...                         |
|  [idle] Copy-code buttons                |  <- Enhancement, deferred
|                                          |
|  ...more content...                      |
|                                          |
|  [visible] Related articles              |  <- Below fold, on scroll
|  [visible] Comment section               |  <- Below fold, on scroll
+------------------------------------------+
|  [media max-width:768px] Mobile footer   |  <- Mobile only
+------------------------------------------+

The page loads fast because only the navigation and dark mode toggle hydrate immediately. Everything else waits until it is needed.

Overriding Plugin Defaults

Plugins declare a default_hydrate strategy in their plugin.toml. Templates can override this per-instance using the data-hydrate attribute:

# plugin.toml -- plugin author's default. The name is namespaced by the plugin,
# because island names share one global registry per page.
[islands."my-plugin:word-count"]
js = "assets/word-count.js"
default_hydrate = "idle"
<!-- Template -- site author's override for a specific use case -->
<accent-island data-component="my-plugin:word-count" data-hydrate="load">
  <span class="word-count">...</span>
</accent-island>

The data-hydrate attribute on the element always wins over the plugin’s default. This lets site authors fine-tune hydration per page without changing the plugin.

Fallback Behavior

When a browser does not support the required API, the island loader falls back gracefully:

StrategyMissing APIFallback
idlerequestIdleCallbacksetTimeout(200)
visibleIntersectionObserverload (immediate)
mediamatchMediaDoes not hydrate

The media strategy intentionally does not fall back to load. If the browser cannot evaluate the media query, the safest default is to leave the static HTML in place rather than hydrate unconditionally.