Islands hydration
Ship static HTML, hydrate only interactive regions. The Island wrapper delays hydration until a strategy fires — load, idle, visible, media, or interaction. Below the triggering point the island is just markup; the JS chunk is not even fetched.
Why islands instead of SSR?
Basic use
import { Island } from '@jorvel/runtime';
<Island
strategy="visible"
load={() => import('./Carousel.js')}
fallback={<CarouselSkeleton />}
/>Strategies
| Strategy | Fires when | Notes |
|---|---|---|
load | As soon as the client mounts | Hydrates synchronously after initial paint |
idle | Next requestIdleCallback | Falls back to setTimeout(0) on Safari < 17 |
visible | Enters the viewport (IntersectionObserver) | Configurable rootMargin via strategyOptions |
media | Media query matches (e.g. (min-width: 768px)) | Re-evaluated on viewport resize |
interaction | User hovers, focuses, clicks, or touches | Click is special-cased: the original click is re-dispatched after hydration |
Props
interface IslandProps {
strategy: 'load' | 'idle' | 'visible' | 'media' | 'interaction';
load: () => Promise<{ default: ComponentType<any> }>;
props?: Record<string, unknown>; // forwarded to the hydrated component
fallback?: ReactNode; // rendered server-side + before hydration
strategyOptions?: {
rootMargin?: string; // 'visible'
threshold?: number; // 'visible'
query?: string; // 'media' (overrides default)
events?: ('mouseenter'|'focus'|'click'|'touchstart')[]; // 'interaction'
};
onHydrate?: () => void; // observability hook
}Mark client boundaries
clientBoundary() tags a component as interactive. The marker is consumed by future build tooling that will auto-wrap flagged components in an Island; for now it's informational and useful as a code-review signal.
import { clientBoundary } from '@jorvel/runtime';
const Counter = clientBoundary(function Counter() {
const [n, set] = React.useState(0);
return <button onClick={() => set(n + 1)}>{n}</button>;
});SSR fallback
The fallback prop is rendered on the server and before hydration. Pass the same HTML the component produces, or a skeleton. After the strategy fires the real component replaces it — React reconciles, so a matching skeleton means a zero-flash swap.
Picking a strategy
| Strategy | Use it for | Avoid for |
|---|---|---|
load | Critical interactive widgets above the fold (login form) | Below-the-fold content (wastes bandwidth) |
idle | Non-critical analytics, chat widgets | Widgets a user clicks within 1s of paint |
visible | Carousels, comments, related posts | Content the user needs before scrolling |
media | Mobile menus, sidebar at min-width: 768px | Anything visible at all viewports (always hydrates) |
interaction | Date pickers, code editors, modals | Components needing keyboard-shortcut bindings on load |
Composing with remotes
An Island can load a remote module. Pair this with strategy="visible" to keep an entire remote off the wire until it scrolls into view.
<Island
strategy="visible"
load={() => import('dashboard/UsageChart')}
fallback={<div className="chart-skeleton" aria-busy />}
/>Interaction strategy: event replay
Under strategy="interaction", the click that triggers hydration is captured and replayed after the component mounts. The end user perceives a normal click — no double-tap, no skipped action. This works for click; other events (focus, mouseenter) are intent signals only and don't replay.
Custom hydration trigger
Pair the load prop with an external signal (e.g. a feature flag, an event bus message) by wrapping Island in a conditional render. The hydration trigger becomes whatever event flips the surrounding state.
function MaybeChart() {
const flagOn = useFeatureFlag('charts');
if (!flagOn) return null;
return (
<Island
strategy="visible"
load={() => import('./Chart.js')}
fallback={<ChartSkeleton />}
/>
);
}Measuring hydration cost
Pass onHydrateto forward each island's hydration to your observability adapter. Aggregate per island name and you have a budget you can hold the line on.
<Island
strategy="visible"
load={() => import('./Carousel.js')}
fallback={<CarouselSkeleton />}
onHydrate={() => reportMetric({ name: 'jorvel.island.hydrate', tags: { name: 'carousel' } })}
/>Match server and client output
fallbackdiffers from the hydrated component's first frame, React will warn about hydration mismatches. Use a static skeleton, not a partial render of the real component.