Selective Hydration & Islands Architecture
Master ForgeWP's islands architecture: automatic interactivity detection, the <Hydrate> boundary, performance triggers, and predictive preloading.
One of the greatest architectural challenges in modern frontend development is performance bloat. When traditional Single Page Application (SPA) frameworks build a website, they force the user's browser to download, parse, and execute hundreds of kilobytes of JavaScript just to render static headings, blog articles, and footers. This hurts Core Web Vitals, increases Time to Interactive (TTI), and wastes mobile battery life.
ForgeWP uses an Islands Architecture: interactive regions receive browser JavaScript while the surrounding theme can remain server-rendered. The exported theme uses PHP-backed templates with block-editor integration.
During export, ForgeWP pre-renders your entire theme to native HTML, CSS, and PHP. Visitors receive server-rendered markup instantly. Client-side JavaScript is sent only for isolated interactive components—called Islands—floating in a sea of static server-rendered HTML.
1. How ForgeWP Discovers Islands (Smart Discovery)
In frameworks like Next.js, developers must manually annotate interactive components with a 'use client' directive at the top of every file. Forget it, and your build throws errors or leaks client code to the server.
ForgeWP uses Smart Discovery: an intelligent Abstract Syntax Tree (AST) compiler engine that automatically detects whether a component requires browser execution.
What Triggers Automatic Island Detection?
The compiler scans components in src/app, src/components, and src/blocks for any of the following interactive patterns:
- Interactive React Hooks:
useState,useEffect,useRef,useReducer,useLayoutEffect,useSyncExternalStore, or customuse*hooks that wrap them. - Event Handlers: Inline handlers like
onClick,onSubmit,onKeyDown,onChange(excluding<WpEditable>'s editor-onlyonChange). - Interactive Data Fetching: Any component using
useWpQuery()(which requires client hydration to fetch and paginate posts). - Motion Directives & Packages: Imports from popular animation libraries (
motion,motion/react,framer-motion,gsap,@react-spring/web,lenis,swiper,lottie-react, etc.) or JSX animation directives (whileInView,whileHover,animate).
Server-Safe Hooks (0 KB Client JS)
ForgeWP recognizes that many hooks are purely read-only and safe to evaluate synchronously at compile time. Using these hooks does not mark a component as interactive:
├── WordPress Data Hooks: useWpTitle, useWpContent, useWpExcerpt, useWpPermalink,
│ useWpDate, useWpAuthor, useWpFeaturedImage, useWpCustomField,
│ useWpOption, useWpThemeMod, useWpMenu, useWpMeta
├── Localization Hooks: useWpI18n, useWpLanguage
├── Motion Safety: useReducedMotion (defaults to calm state in static pass)
└── Safe React Hooks: useId, useMemo, useCallback, useTransition, useDeferredValueIf a component uses only server-safe hooks and static markup, ForgeWP renders it to native PHP/HTML and ships zero JavaScript for that component.
2. Automatic Island Splitting
What happens when a large, mostly static page contains just one interactive widget (such as a newsletter form or a tab switch)?
In older architectures, you had to manually extract that widget into a separate file and wrap it in a boundary. ForgeWP's compiler features an Automatic Island Splitter:
- Subtree Analysis: The compiler identifies static parent markup and isolates the nested interactive subtree.
- Transparent Extraction: During build preflight, the compiler creates a dedicated client chunk inside
.forgewp-islands/. - Static Shell Preservation: The parent template remains 100% static server-rendered HTML with an auto-wrapped
<Hydrate>marker around the extracted widget. - Layout Geometry Preservation: The compiler extracts layout-critical utility classes (
w-full,sticky,flex,grid) from the component's root and passes them to the island container, preventing layout collapse or Cumulative Layout Shift (CLS).
3. Scaffolding an Island Component
To scaffold an interactive component with starter state, run the CLI generator:
pnpm forgewp make:island CounterIslandThis creates src/components/CounterIsland.tsx:
1import { useState } from "react"
2
3export function CounterIsland() {
4 const [count, setCount] = useState(0)
5
6 return (
7 <div className="flex items-center gap-4 rounded-xl border border-border bg-card p-4">
8 <span className="text-sm font-medium text-foreground">
9 Current clicks: <strong>{count}</strong>
10 </span>
11 <button
12 onClick={() => setCount((prev) => prev + 1)}
13 className="rounded-lg bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition hover:opacity-90"
14 >
15 Increment
16 </button>
17 </div>
18 )
19}Because this component uses useState and onClick, ForgeWP automatically detects it as an interactive island. You can render it directly in your page without any manual wrapping:
1import { CounterIsland } from "@/components/CounterIsland"
2
3export default function HomePage() {
4 return (
5 <main className="mx-auto max-w-4xl p-8">
6 <h1 className="text-3xl font-bold">Welcome to our site</h1>
7 {/* Automatically discovered and hydrated as an island */}
8 <CounterIsland />
9 </main>
10 )
11}4. Fine-Tuning Interactivity with <Hydrate>
While Smart Discovery handles island detection automatically, you often want precise control over when and under what conditions an island loads.
Import the <Hydrate> component from @forgewp/react:
1import { Hydrate } from "@forgewp/react"
2import { ImageGallery } from "@/components/ImageGallery"
3
4export default function HomePage() {
5 return (
6 <main>
7 {/* Hydrate only when scrolled into the viewport */}
8 <Hydrate trigger="visible" preload="near-visible">
9 <ImageGallery />
10 </Hydrate>
11 </main>
12 )
13}Complete <Hydrate> Props Reference
Prop Type Default Description trigger'visible' | 'load' | 'interaction' | 'click' | 'hover' | 'idle''visible'The browser event or condition that initiates client-side hydration. preload'near-visible' | 'none''none'Preloads the JavaScript bundle when the element is within 600px of the viewport before full intersection. clientOnlybooleanfalseBypasses SSR pre-rendering completely. Renders a clean placeholder on the server and mounts exclusively in the browser. mediastringundefinedRestricts hydration to devices matching a CSS media query (e.g. "(max-width: 768px)").connection'fast' | 'any''any'When set to 'fast', skips or defers hydration on slow 2G/3G networks or when Save-Data is enabled.idstringundefinedExplicit bundle chunk identifier. If omitted, ForgeWP derives it from the child component's name. classNamestringundefinedCSS classes applied directly to the island container wrapper. styleCSSPropertiesundefinedInline styles merged with the wrapper's default display: contents.needsProvidersbooleanfalseWraps the island in the application's layout providers (scanned from src/app/layout.tsx).slotChildrenbooleanfalseRe-feeds the server-rendered inner HTML as children to wrapper components (e.g. 3D tilt cards). |
|---|
5. The Six Hydration Strategies (trigger)
Choosing the right trigger ensures non-critical JavaScript never blocks the main thread during initial page load:
1. visible (Default & Recommended)
Hydrates lazily when the component enters the viewport using an internal IntersectionObserver with a generous 200px threshold. Ideal for below-the-fold image sliders, reviews carousels, and complex charts.
2. load
Hydrates immediately after window.onload fires (or instantly if the page is already complete). Reserve this for critical above-the-fold interactive elements, such as announcement bars or hero search filters.
3. idle
Hydrates during browser idle periods using requestIdleCallback (with a 200ms fallback for older browsers). Ideal for non-critical widgets like chat bubbles, cookie banners, or analytics dashboards.
4. interaction
Hydrates only when the user hovers over, focuses, or clicks inside the component (click, mouseenter, or focusin). Perfect for mega menus, mobile drawers, or search popups.
5. click
Hydrates exclusively when the user clicks the element. Ideal for modals, confirmation dialogs, and expandable FAQ accordions.
6. hover
Hydrates on mouseenter. Excellent for hover-card previews, tooltip cards, and thumbnail magnification previews.
6. Advanced Performance Controls
Predictive Preloading (preload="near-visible")
Waiting until an element is fully visible before fetching its JavaScript chunk can create a small delay if the user scrolls quickly. With preload="near-visible", ForgeWP sets up a secondary observer with a 600px root margin:
1<Hydrate trigger="visible" preload="near-visible">
2 <InteractiveCommentsList />
3</Hydrate>The browser downloads the JavaScript bundle in the background while the user is still 600px away. By the time the component scrolls into view, the bundle is cached and ready to execute instantly.
Browser-Only Components (clientOnly)
Some third-party libraries rely strictly on browser APIs (window, document, HTML5 Canvas, WebGL, Leaflet maps, Chart.js) and crash if executed in Node.js during export.
Set clientOnly to bypass SSR entirely:
1import { Hydrate } from "@forgewp/react"
2import { LeafletMap } from "./LeafletMap"
3
4export function MapSection() {
5 return (
6 <section className="h-96 w-full">
7 <Hydrate trigger="visible" clientOnly>
8 <LeafletMap />
9 </Hydrate>
10 </section>
11 )
12}During export, ForgeWP emits a lightweight placeholder element. Once the page loads in the browser, the component mounts smoothly with zero SSR warnings.
Device and Network Adaptability
You can prevent heavy client widgets from draining bandwidth on mobile devices or metered connections:
1{/* Hydrate only on mobile/tablet screens */}
2<Hydrate trigger="load" media="(max-width: 768px)">
3 <MobileDrawerNavigation />
4</Hydrate>
5
6{/* Defer or skip hydration on slow 2G/3G or Save-Data mode */}
7<Hydrate trigger="visible" connection="fast">
8 <HighRes3DModelViewer />
9</Hydrate>7. Critical Rule: Interactive Handlers inside <WpQueryLoop>
A common pitfall occurs when developers place interactive click handlers or buttons inside a <WpQueryLoop>:
1<WpQueryLoop postType="post" postsPerPage={6}>
2 {/* This onClick will be IGNORED on live WordPress without a hydration boundary! */}
3 <button onClick={() => bookmarkPost()}>
4 Bookmark
5 </button>
6</WpQueryLoop>Why Does This Happen?
<WpQueryLoop> compiles to a native WordPress PHP loop (while ( have_posts() ) : the_post();). In production, this loop executes on the server in PHP. If the loop body contains event handlers without an explicit hydration boundary, the PHP server renders the button as inert HTML, and the client JavaScript is never attached.
The Solution:
Wrap the loop or card in a <Hydrate> boundary:
1<Hydrate trigger="visible">
2 <WpQueryLoop postType="post" postsPerPage={6}>
3 <InteractivePostCard />
4 </WpQueryLoop>
5</Hydrate>ForgeWP's Static Linter automatically analyzes your components during pnpm export and will flag any unhydrated interactive handlers inside <WpQueryLoop> with a compile-time warning.
8. Summary Decision Flowchart
Use this decision flowchart when architecting components in your ForgeWP theme:
Should I wrap every component in `<Hydrate>`?
No! By default, let ForgeWP's Smart Discovery handle components automatically. Reach for <Hydrate> when you want to delay execution (e.g. trigger="idle" or trigger="visible"), preload dynamic scripts ahead of time, or isolate browser-only libraries with clientOnly.
Build and practice an island
Follow Build a Mobile Menu Island to combine a native disclosure with Escape handling. Then practice an arrival-information dialog and compare your implementation with its worked solution. Each lesson includes exported-site checks for keyboard use, focus, and small screens.