Server Islands allow you to embed dynamic, server-rendered components within static pages. This gives you the best of both worlds: fast static page loads with selective server-rendered content for personalization, real-time data, or authentication.
What are Server Islands?
Server Islands are components that render on the server for each request, even when the rest of the page is statically generated. They load asynchronously after the initial page render, providing dynamic content without blocking the main page load.
Server Islands require an SSR adapter and work in output: 'static' or output: 'hybrid' modes.
Creating a Server Island
Mark any component as a server island using the server:defer directive:
---
import UserProfile from '../components/UserProfile.astro' ;
import RealtimeStock from '../components/RealtimeStock.astro' ;
---
< html >
< head >
< title > My Site </ title >
</ head >
< body >
< h1 > Welcome to My Site </ h1 >
<!-- Static content renders immediately -->
< section class = "hero" >
< p > This content is static and super fast! </ p >
</ section >
<!-- Server island renders on each request -->
< UserProfile server:defer />
<!-- Another server island with real-time data -->
< RealtimeStock symbol = "ASTRO" server:defer />
<!-- More static content -->
< footer >
< p > © 2026 My Site </ p >
</ footer >
</ body >
</ html >
Server Island Component
Server islands are just regular Astro components that can access server-side features:
src/components/UserProfile.astro
---
const { cookies } = Astro ;
const userId = cookies . get ( 'userId' )?. value ;
let user = null ;
if ( userId ) {
user = await db . users . find ( userId );
}
---
{ user ? (
< div class = "profile" >
< img src = { user . avatar } alt = { user . name } />
< h2 > Welcome back, { user . name } ! </ h2 >
< p > Last login: {new Date ( user . lastLogin ). toLocaleString () } </ p >
</ div >
) : (
< div class = "login-prompt" >
< p > Please log in to see your profile </ p >
< a href = "/login" > Log In </ a >
</ div >
) }
< style >
.profile {
border : 1 px solid #ddd ;
padding : 1 rem ;
border-radius : 8 px ;
}
</ style >
How It Works
Initial Page Load
The static HTML is served immediately with a placeholder for the server island. < astro-island uid = "abc123" component-url = "/_server-islands/UserProfile" >
< div > Loading... </ div >
</ astro-island >
Server Rendering
The browser requests the server island content via a background fetch: GET /_server-islands/UserProfile?s=encrypted_slots&p=encrypted_props
Hydration
The server responds with rendered HTML, which replaces the placeholder: < div class = "profile" >
< img src = "/avatar.jpg" alt = "John" />
< h2 > Welcome back, John! </ h2 >
</ div >
Provide fallback content that shows while loading: < UserProfile server:defer >
< div slot = "fallback" >
< p > Loading profile... </ p >
</ div >
</ UserProfile >
Use Cases
Personalization Show user-specific content like profiles, recommendations, or preferences
Real-time Data Display live data like stock prices, availability, or analytics
Authentication Render auth-protected content based on session state
A/B Testing Serve different variants for experiments
Passing Props
Pass props to server islands just like regular components:
src/pages/product/[id].astro
---
import StockStatus from '../../components/StockStatus.astro' ;
const { id } = Astro . params ;
const product = await getProduct ( id );
---
< html >
< body >
< h1 > { product . name } </ h1 >
< p > { product . description } </ p >
<!-- Pass product ID to server island -->
< StockStatus productId = { id } server:defer >
< div slot = "fallback" > Checking availability... </ div >
</ StockStatus >
</ body >
</ html >
src/components/StockStatus.astro
---
interface Props {
productId : string ;
}
const { productId } = Astro . props ;
// Fetch real-time stock data
const stock = await inventory . check ( productId );
---
< div class = "stock-status" >
{ stock . available ? (
< span class = "in-stock" >
✓ In Stock ( { stock . quantity } available)
</ span >
) : (
< span class = "out-of-stock" >
✗ Out of Stock
</ span >
) }
</ div >
Props are encrypted during transmission for security. Don’t pass sensitive data that shouldn’t be cached.
Slots
Use slots to pass content to server islands:
src/pages/dashboard.astro
---
import AdminPanel from '../components/AdminPanel.astro' ;
---
< AdminPanel server:defer >
< div slot = "fallback" >
< p > Loading admin panel... </ p >
</ div >
< h2 > Admin Controls </ h2 >
< p > Manage your site here </ p >
</ AdminPanel >
src/components/AdminPanel.astro
---
const user = Astro . locals . user ;
if ( ! user ?. isAdmin ) {
return Astro . redirect ( '/forbidden' );
}
---
< div class = "admin-panel" >
< slot />
<!-- Server-rendered admin data -->
< div class = "stats" >
< p > Active users: { await db . users . countActive () } </ p >
< p > Pending reviews: { await db . reviews . countPending () } </ p >
</ div >
</ div >
Configuration
Enable server islands in your Astro config:
import { defineConfig } from 'astro/config' ;
import node from '@astrojs/node' ;
export default defineConfig ({
output: 'hybrid' ,
adapter: node () ,
experimental: {
serverIslands: true
}
}) ;
Server islands work best with output: 'hybrid' for mixing static and dynamic content.
Combining with Client Islands
Mix server islands with client-side framework components:
---
import UserData from '../components/UserData.astro' ;
import InteractiveChart from '../components/Chart.tsx' ;
---
< html >
< body >
<!-- Server island: personalized data -->
< UserData server:defer />
<!-- Client island: interactive UI -->
< InteractiveChart client:load />
<!-- Static content -->
< section class = "info" >
< h2 > About Our Service </ h2 >
< p > Static marketing content... </ p >
</ section >
</ body >
</ html >
Render on server for each request
Access databases and sessions
Load asynchronously
No JavaScript hydration needed
Render on client
Interactive components
Require JavaScript
Hydrate in the browser
Caching Strategies
Control caching for server islands:
src/components/CachedWidget.astro
---
// Set cache headers for this server island
Astro . response . headers . set ( 'Cache-Control' , 'public, max-age=60' );
const data = await fetchExpensiveData ();
---
< div class = "widget" >
< h3 > { data . title } </ h3 >
< p > Updated: {new Date (). toLocaleTimeString () } </ p >
</ div >
Use with server:defer:
< CachedWidget server:defer />
Error Handling
Handle errors gracefully in server islands:
src/components/WeatherWidget.astro
---
interface Props {
city : string ;
}
const { city } = Astro . props ;
let weather ;
let error ;
try {
weather = await fetchWeather ( city );
} catch ( e ) {
error = e . message ;
}
---
< div class = "weather" >
{ error ? (
< div class = "error" >
< p > Failed to load weather: { error } </ p >
< button onclick = "location.reload()" > Retry </ button >
</ div >
) : (
< div class = "data" >
< h3 > { weather . city } </ h3 >
< p > { weather . temperature } °F </ p >
< p > { weather . condition } </ p >
</ div >
) }
</ div >
Loading States
Provide meaningful loading states:
---
import RecentActivity from '../components/RecentActivity.astro' ;
---
< RecentActivity server:defer >
< div slot = "fallback" class = "skeleton" >
< div class = "skeleton-line" ></ div >
< div class = "skeleton-line" ></ div >
< div class = "skeleton-line" ></ div >
</ div >
</ RecentActivity >
< style >
.skeleton-line {
height : 20 px ;
background : linear-gradient ( 90 deg , #f0f0f0 25 % , #e0e0e0 50 % , #f0f0f0 75 % );
background-size : 200 % 100 % ;
animation : loading 1.5 s infinite ;
margin-bottom : 10 px ;
border-radius : 4 px ;
}
@keyframes loading {
0% { background-position : 200 % 0 ; }
100% { background-position : -200 % 0 ; }
}
</ style >
Advanced Example
A complete example combining multiple concepts:
src/pages/dashboard.astro
---
import UserStats from '../components/UserStats.astro' ;
import RealtimeNotifications from '../components/Notifications.astro' ;
import ActivityFeed from '../components/ActivityFeed.astro' ;
---
< html >
< head >
< title > Dashboard </ title >
</ head >
< body >
< header >
< h1 > Dashboard </ h1 >
</ header >
< main class = "grid" >
<!-- User-specific stats -->
< UserStats server:defer >
< div slot = "fallback" > Loading stats... </ div >
</ UserStats >
<!-- Real-time notifications -->
< RealtimeNotifications server:defer >
< div slot = "fallback" > Loading notifications... </ div >
</ RealtimeNotifications >
<!-- Activity feed -->
< ActivityFeed server:defer >
< div slot = "fallback" > Loading activity... </ div >
</ ActivityFeed >
</ main >
</ body >
</ html >
< style >
.grid {
display : grid ;
grid-template-columns : repeat ( auto-fit , minmax ( 300 px , 1 fr ));
gap : 1 rem ;
padding : 1 rem ;
}
</ style >
Best Practices
Keep islands small
Server islands should be focused components. Split large components into multiple islands.
Provide fallbacks
Always show something while loading. Use skeleton screens or loading messages.
Handle errors
Gracefully handle failures and provide retry mechanisms.
Consider caching
Cache server island responses when appropriate to reduce server load.
Monitor performance
Track server island response times and optimize slow queries.
SSR and SSG Understanding rendering modes
Components Building Astro components
Islands Architecture Learn about islands architecture
Middleware Add server-side logic