Server Islands
Server islands let you render expensive or dynamic components on the server without blocking the initial page shell. The page loads instantly with a placeholder, then the island streams in when the server-side data is ready.
Different from
"use client"and"use signals"islands. Server islands are about deferred server rendering — the component still runs on the server, just streamed late so the shell ships first. For interactive islands that hydrate in the browser, see"use client"(React-based) or"use signals"(no React, ~3 KB).
How It Works
Mark a component as a server island with the island directive:
// components/ProductReviews.tsx
"use island";
export default async function ProductReviews({ productId }: { productId: string }) {
// This fetch happens on the server, after the page shell is sent
const reviews = await db.reviews.findMany({ where: { productId } });
return (
<section>
<h3>{reviews.length} Reviews</h3>
{reviews.map((r) => (
<div key={r.id}>
<strong>{r.author}</strong>
<p>{r.body}</p>
</div>
))}
</section>
);
}
// pages/product/[id].tsx
import ProductReviews from "../../components/ProductReviews";
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<main>
<h1>Product Details</h1>
<p>This part renders immediately.</p>
{/* This streams in after the data fetch completes */}
<ProductReviews productId={params.id} />
</main>
);
}
Rendering Behavior
- Page shell — bext renders the full page, replacing islands with a
<bext-island>placeholder element - Streaming — each island renders in parallel on the server via the V8 pool
- Injection — completed islands stream into the page as
<script>tags that replace their placeholder - Hydration — if the island has client-side interactivity, React hydrates it after injection
This means:
- Time to first byte is fast because the shell doesn't wait for slow data
- Islands render in parallel, not sequentially — a slow database query in one island doesn't block others
- SEO is preserved — the final HTML contains the full rendered content for crawlers
Configuration
# bext.config.toml
[render]
islands = true # default: auto-detected from "use island" directives
island_timeout_ms = 10000 # timeout per island (renders fallback on timeout)
island_concurrency = 8 # max parallel island renders per request
Fallback Content
Provide a fallback that shows while the island is loading or if it times out:
<ProductReviews productId={id} fallback={<p>Loading reviews...</p>} />
If the island fails or times out, the fallback stays in place and an error is logged. The rest of the page is unaffected.
When to Use Server Islands
| Use case | Recommendation |
|---|---|
| Personalized content (cart, user profile) | Server island — avoids caching issues |
| Slow database queries | Server island — doesn't block the page shell |
| Static content | Regular SSR — no need to defer |
| Real-time data | Combine with SSE via the real-time hub |
| Below-the-fold content | Server island — improves perceived performance |
Comparison with Streaming SSR
Server islands differ from React's built-in streaming (via Suspense boundaries):
- Islands are component-level — you mark individual components, not Suspense boundaries
- Islands run in parallel — the V8 pool distributes renders across workers
- Islands support timeout + fallback — graceful degradation is built in
- Islands work without React — they're a bext platform feature, not tied to a framework
Each island render occupies a slot in the shared V8 pool. If a page has many
islands and the pool is saturated, additional islands queue rather than run
truly in parallel. Tune island_concurrency and the pool size together (see
V8 render pool).
Related
- Frameworks — Islands —
"use client"interactive islands for browser hydration - Frameworks — Signals —
"use signals"reactive islands (~3 KB, no React) - V8 render pool — pool sizing and concurrency that back parallel island renders
- Server Actions — complement islands with form actions for mutations
- Caching — cache island output independently to avoid repeated server renders