The Islands Architecture
Traditional SPAs force you to download JavaScript for your entire page before showing anything interactive. A shopping cart button needs the cart logic. But why should that button force users to download navigation code, footer code, and every other component? That's wasted bandwidth and slower page loads.
Interactive islands flip this model. Your page renders as static HTML on the server, sending complete content to browsers within milliseconds. Then small interactive components (islands) activate independently, downloading only the code they need. A search box island loads search logic. A cart button island loads cart logic. The footer stays static because it doesn't need any JavaScript.
You'll build pages using the islands pattern, learning where to place islands, how to size them correctly, and when this approach beats fully interactive pages. By the end, you'll know how to build fast-loading pages that feel interactive where it matters.
Your First Interactive Island
Start with a static SSR page that loads instantly. Identify one component that needs interactivity, like a button or form. Extract that component into a separate file and add the @rendermode directive. The rest of the page stays static while just that component becomes interactive.
This focused approach means users see your page content immediately. While they read the static content, the interactive island initializes in the background. By the time they're ready to interact, the island is responsive and ready.
@page "/article/{id:int}"
@inject IArticleService Articles
@* Static SSR page wrapper *@
<PageTitle>@article?.Title ?? "Article"</PageTitle>
@if (article != null)
{
<!-- Static content: loads instantly -->
<article class="content">
<h1>@article.Title</h1>
<p class="meta">By @article.Author | @article.Date</p>
<div class="body">
@((MarkupString)article.HtmlContent)
</div>
</article>
<!-- Interactive island: activates after page loads -->
<LikeButton ArticleId="@article.Id"
InitialLikes="@article.LikeCount" />
<!-- Static comments display -->
<section class="comments">
<h2>Comments (@article.Comments.Count)</h2>
@foreach (var comment in article.Comments)
{
<div class="comment">
<strong>@comment.Author</strong>
<p>@comment.Text</p>
</div>
}
</section>
<!-- Another interactive island: comment form -->
<CommentForm ArticleId="@article.Id" />
}
@code {
[Parameter]
public int Id { get; set; }
private Article? article;
protected override async Task OnInitializedAsync()
{
article = await Articles.GetByIdAsync(Id);
}
}
The article page loads all content statically: title, author, date, body text, and existing comments. Users see everything within 100ms. Two small islands provide interactivity: a like button and a comment form. These activate in the background without blocking the initial content display.
Building a Simple Island Component
Interactive islands should be small, focused components with clear boundaries. They receive initial state through parameters from the static parent page, then handle their own interactions independently. Keep islands self-contained to avoid coordination complexity.
Use Server mode for islands that need occasional interaction. The setup is instant and the round-trip delay is acceptable for actions like clicking a like button or submitting a comment. Users don't notice the 50-100ms latency for these infrequent actions.
@rendermode RenderMode.InteractiveServer
@inject IArticleService Articles
<div class="like-button-island">
<button @onclick="ToggleLike" disabled="@isProcessing"
class="@(hasLiked ? "liked" : "")">
<span class="icon">@(hasLiked ? "❤️" : "🤍")</span>
<span class="count">@likeCount</span>
</button>
</div>
@code {
[Parameter, EditorRequired]
public int ArticleId { get; set; }
[Parameter]
public int InitialLikes { get; set; }
private int likeCount;
private bool hasLiked = false;
private bool isProcessing = false;
protected override void OnInitialized()
{
likeCount = InitialLikes;
}
private async Task ToggleLike()
{
isProcessing = true;
try
{
if (hasLiked)
{
await Articles.RemoveLikeAsync(ArticleId);
likeCount--;
hasLiked = false;
}
else
{
await Articles.AddLikeAsync(ArticleId);
likeCount++;
hasLiked = true;
}
}
finally
{
isProcessing = false;
}
}
}
The like button island receives the initial like count from the parent page through a parameter. It manages its own state for the current count and whether the user has liked the article. When clicked, it calls the service to update the server and refreshes its display. The 50ms round trip is invisible to users for this type of interaction.
WebAssembly Islands for Rich Interactions
Some islands need instant responsiveness without network delays. A color picker, drawing tool, or live calculator benefits from running entirely in the browser. For these cases, create islands using WebAssembly mode instead of Server mode.
WebAssembly islands cost more upfront because browsers must download the .NET runtime and your component code. However, once loaded, they provide zero-latency interactions. Users can drag sliders, type in fields, or interact with visualizations with immediate feedback.
@rendermode RenderMode.InteractiveWebAssembly
<div class="calculator-island">
<h3>Estimate Your Total</h3>
<div class="calc-input">
<label>
Base Price: $@basePrice.ToString("F2")
<input type="range" @bind="basePrice" @bind:event="oninput"
min="10" max="1000" step="10" />
</label>
</div>
<div class="calc-input">
<label>
Quantity: @quantity
<input type="range" @bind="quantity" @bind:event="oninput"
min="1" max="100" />
</label>
</div>
<div class="calc-input">
<label>
<input type="checkbox" @bind="includeShipping" />
Include shipping ($@shippingCost.ToString("F2"))
</label>
</div>
<div class="calc-result">
<h4>Subtotal: $@subtotal.ToString("F2")</h4>
<p>Tax (@(taxRate * 100)%): $@tax.ToString("F2")</p>
@if (includeShipping)
{
<p>Shipping: $@shippingCost.ToString("F2")</p>
}
<h3>Total: $@total.ToString("F2")</h3>
</div>
</div>
@code {
private decimal basePrice = 100;
private int quantity = 1;
private bool includeShipping = false;
private decimal shippingCost = 15;
private decimal taxRate = 0.08m;
private decimal subtotal => basePrice * quantity;
private decimal tax => subtotal * taxRate;
private decimal total =>
subtotal + tax + (includeShipping ? shippingCost : 0);
}
Users drag the sliders and see the total update instantly without any network delay. Every slider movement recalculates the totals in the browser. This immediate feedback makes the calculator feel smooth and responsive. The WebAssembly download is justified because users interact frequently with the calculator during their session.
When Islands Need to Talk
Keep islands independent whenever possible. Each island should manage its own state and interactions without depending on other islands. This simplicity makes your page faster and easier to reason about.
When islands must communicate, use browser storage as the message bus. One island saves data to localStorage and raises a storage event. Other islands listen for storage events and update accordingly. This works across different render modes and keeps islands loosely coupled.
@rendermode RenderMode.InteractiveServer
@inject IJSRuntime JS
@implements IDisposable
<div class="cart-counter-island">
<button @onclick="ViewCart">
🛒 Cart (@itemCount)
</button>
</div>
@code {
private int itemCount = 0;
private DotNetObjectReference<CartCounter>? objRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Load initial count from storage
var stored = await JS.InvokeAsync<string>(
"localStorage.getItem", "cartCount");
if (int.TryParse(stored, out int count))
{
itemCount = count;
StateHasChanged();
}
// Listen for cart updates from other islands
objRef = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("window.addEventListener",
"storage", objRef);
}
}
[JSInvokable]
public async Task OnStorageChanged()
{
var stored = await JS.InvokeAsync<string>(
"localStorage.getItem", "cartCount");
if (int.TryParse(stored, out int count))
{
itemCount = count;
StateHasChanged();
}
}
private void ViewCart()
{
// Navigate to cart page
}
public void Dispose()
{
objRef?.Dispose();
}
}
This cart counter island listens for storage events to update its count when other islands add items to the cart. Multiple islands can update the cart independently, and all counter islands will reflect the new total. This loose coupling keeps each island focused on its own responsibility.
Build an Islands Page
Create a blog post page with static content and interactive islands.
Steps
- Create:
dotnet new blazor -n IslandsDemo
- Navigate:
cd IslandsDemo
- Add the components below
- Run:
dotnet run
- Open:
https://localhost:5001/post
- View source to see pre-rendered content
- Watch Network tab to see islands activate
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
@page "/post"
<PageTitle>Blog Post - Islands Demo</PageTitle>
<!-- Static content loads instantly -->
<article style="max-width: 800px; margin: 0 auto; padding: 20px;">
<h1>Understanding Interactive Islands</h1>
<p class="meta" style="color: #666;">
By Jane Developer | March 15, 2024
</p>
<p>
Interactive islands let you build fast-loading pages with targeted
interactivity. Most of your page renders as static HTML while small
components activate to handle user interactions.
</p>
<p>
This approach combines the best of static site generation with the
interactivity users expect. You get instant page loads plus responsive
UI elements exactly where you need them.
</p>
<!-- Interactive island: like button -->
<div style="margin: 30px 0;">
<SimpleLikeButton />
</div>
<h2>Why This Matters</h2>
<p>
Traditional SPAs download JavaScript for your entire app before showing
content. Islands flip this: show content immediately, then activate
interactivity progressively.
</p>
</article>
@* Create Components/SimpleLikeButton.razor:
@rendermode RenderMode.InteractiveServer
<div style="border: 2px solid #ddd; padding: 15px; border-radius: 8px;">
<p><strong>Did you find this helpful?</strong></p>
<button @onclick="IncrementLikes"
style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
👍 Helpful (@likeCount)
</button>
</div>
@code {
private int likeCount = 42;
private void IncrementLikes() => likeCount++;
}
*@
What you'll see
The article content appears instantly. View source shows complete HTML with the title, author, date, and paragraphs already rendered. The like button island establishes a SignalR connection after the page loads. Click the button and it increments immediately, showing the Server mode round trip in the Network tab.
Design Trade-offs
Choose islands over fully interactive pages when most of your content is static. Blog posts, product details, documentation, and marketing pages benefit from islands because they're primarily content with a few interactive elements. The static content loads instantly while islands activate in the background.
Choose fully interactive pages when most components need to respond to user input. Admin dashboards, data entry forms, and collaborative tools often need widespread interactivity. Making everything an island adds unnecessary complexity when a fully interactive page is simpler and clearer.
Keep islands small and focused. An island should handle one specific interaction: submitting a form, toggling a setting, or adding an item to a cart. Avoid creating large islands that contain multiple unrelated features. Smaller islands activate faster and are easier to maintain.
Measure the impact of your islands. Use browser DevTools to check when each island becomes interactive. If your islands delay content visibility beyond 300ms, reconsider your approach. The goal is fast content display with progressive interactivity, not replacing SPAs with a more complex architecture.