JORVEL

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?

Streaming SSR hydrates the entire tree once it lands. Islands shift cost: zero JS by default, and only the components a user actually reaches pay the hydration tax. Use islands for "mostly-static, locally-interactive" pages (marketing, docs, product pages). Use streaming SSR for "mostly-dynamic" pages (dashboards).

Basic use

tsx
import { Island } from '@jorvel/runtime';

<Island
  strategy="visible"
  load={() => import('./Carousel.js')}
  fallback={<CarouselSkeleton />}
/>

Strategies

StrategyFires whenNotes
loadAs soon as the client mountsHydrates synchronously after initial paint
idleNext requestIdleCallbackFalls back to setTimeout(0) on Safari < 17
visibleEnters the viewport (IntersectionObserver)Configurable rootMargin via strategyOptions
mediaMedia query matches (e.g. (min-width: 768px))Re-evaluated on viewport resize
interactionUser hovers, focuses, clicks, or touchesClick is special-cased: the original click is re-dispatched after hydration

Props

ts
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.

tsx
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

StrategyUse it forAvoid for
loadCritical interactive widgets above the fold (login form)Below-the-fold content (wastes bandwidth)
idleNon-critical analytics, chat widgetsWidgets a user clicks within 1s of paint
visibleCarousels, comments, related postsContent the user needs before scrolling
mediaMobile menus, sidebar at min-width: 768pxAnything visible at all viewports (always hydrates)
interactionDate pickers, code editors, modalsComponents 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.

tsx
<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.

tsx
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.

tsx
<Island
  strategy="visible"
  load={() => import('./Carousel.js')}
  fallback={<CarouselSkeleton />}
  onHydrate={() => reportMetric({ name: 'jorvel.island.hydrate', tags: { name: 'carousel' } })}
/>

Match server and client output

If the server-rendered 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.