Skip to main content
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:
src/pages/index.astro
---
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: 1px solid #ddd;
    padding: 1rem;
    border-radius: 8px;
  }
</style>

How It Works

1

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>
2

Server Rendering

The browser requests the server island content via a background fetch:
GET /_server-islands/UserProfile?s=encrypted_slots&p=encrypted_props
3

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>
  • Initial page loads instantly (static HTML)
  • Server islands load asynchronously
  • No blocking on server-rendered content
  • Main content visible immediately
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:
astro.config.mjs
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:
src/pages/app.astro
---
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:
src/pages/index.astro
---
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: 20px;
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: loading 1.5s infinite;
    margin-bottom: 10px;
    border-radius: 4px;
  }

  @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(300px, 1fr));
    gap: 1rem;
    padding: 1rem;
  }
</style>

Best Practices

1

Keep islands small

Server islands should be focused components. Split large components into multiple islands.
2

Provide fallbacks

Always show something while loading. Use skeleton screens or loading messages.
3

Handle errors

Gracefully handle failures and provide retry mechanisms.
4

Consider caching

Cache server island responses when appropriate to reduce server load.
5

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

Build docs developers (and LLMs) love