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
| Strategy | Activates | Browser API | Cost |
|---|---|---|---|
load | Immediately on page load | Direct call | Blocks initial rendering |
idle | When the browser has spare cycles | requestIdleCallback | Deferred, low priority |
visible | When scrolled into view | IntersectionObserver | Zero cost until visible |
media | When a CSS media query matches | matchMedia | Conditional, 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:
-
Does the user interact with this component immediately on page load? Yes ->
load. (Forms, navigation, consent banners.) -
Is the component visible above the fold? No ->
visible. (Don’t hydrate what the user can’t see.) -
Does the component only apply to certain screen sizes? Yes ->
media. (Mobile menus, touch carousels.) -
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.
Pattern: Progressive Search
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:
| Strategy | Main Thread Impact | Network Impact | When to Worry |
|---|---|---|---|
load | Immediate (blocks TTI) | Script fetched eagerly | More than 2-3 load islands per page |
idle | Deferred (~200ms) | Script fetched eagerly | Many idle islands competing for idle time |
visible | None until scrolled | Script fetched eagerly | Large islands with heavy init logic |
media | None if query misses | Script fetched eagerly | Rarely 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:
| Strategy | Missing API | Fallback |
|---|---|---|
idle | requestIdleCallback | setTimeout(200) |
visible | IntersectionObserver | load (immediate) |
media | matchMedia | Does 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.