Blazor SSR & Interactive Islands: Streaming Rendering, Auto Render Mode & Progressive Enhancement
22 min read
Advanced
Your Blazor app loads in 80 milliseconds. Search engines index every product. Users without JavaScript still see content. Interactive features work instantly after the first visit. This isn't magic. It's Server-Side Rendering with interactive islands.
Most Blazor apps choose between SSR for speed or WebAssembly for richness. You don't have to pick one. Mix both in the same page. Render product listings on the server for SEO. Add a shopping cart as an interactive island. Stream slow database queries while showing instant content. This tutorial shows you how.
What You'll Build
You'll build a production-ready e-commerce product catalog that demonstrates every technique:
Server-rendered product pages that load fast and rank well in search
Interactive shopping cart that works offline with WebAssembly
Streaming product reviews that don't block initial page load
Auto render mode transitions from Server to WebAssembly seamlessly
Progressive enhancement so the site works without JavaScript
Understanding Blazor Render Modes
Blazor .NET 8 gives you four render modes. Each serves a specific purpose. Pick the wrong one and you'll either sacrifice SEO or pay unnecessary performance costs.
Static Server Rendering (SSR)
SSR generates HTML on the server and sends it to the browser. No WebAssembly download. No SignalR connection. Just pure HTML. It's fast, SEO-friendly, and works everywhere. But it's not interactive. Clicking a button requires a full page reload.
Use SSR for product listings, blog posts, documentation, and landing pages. Anything content-heavy where initial load speed and search rankings matter more than interactivity.
SSR Component
@* No rendermode directive = SSR by default *@
@page "/products"
}
@code {
private Product[] products = Array.Empty();
protected override async Task OnInitializedAsync()
{
// Runs on server, renders to HTML
products = await ProductService.GetProductsAsync();
}
}
Interactive Server
InteractiveServer keeps a SignalR WebSocket connection open. When you click a button, the event travels to the server over the wire. The server processes it, calculates the diff, and sends back just the changed HTML. It feels instant for most interactions.
Use InteractiveServer for admin dashboards, real-time data displays, and apps where users stay connected. Don't use it for public-facing sites with thousands of concurrent users. Each connection costs server resources.
Interactive Server Component
@page "/cart"
@rendermode InteractiveServer
Shopping Cart
@foreach (var item in cartItems)
{
@item.Name
}
Total: @total.ToString("C")
@code {
private List cartItems = new();
private decimal total;
private async Task RemoveItem(int id)
{
// Runs on server, UI updates via SignalR
cartItems.RemoveAll(x => x.Id == id);
total = cartItems.Sum(x => x.Price);
await SaveCartAsync();
}
}
Interactive WebAssembly
InteractiveWebAssembly downloads the .NET runtime to the browser (about 1.5MB gzipped). After that, your C# code runs entirely client-side. No server calls for UI updates. It enables offline scenarios and scales to millions of users without server load.
Use WebAssembly for games, design tools, offline apps, and features that need client-side performance. The initial download cost pays off when users interact heavily.
@code {
private int rotation = 0;
private string color = "red";
// Runs in browser, zero server round trips
private string GetPreviewStyle() =>
$"transform: rotate({rotation}deg); background: {color};";
}
Auto Render Mode
Auto mode is the smart default. It starts with SSR for the first request. Then it checks: is WebAssembly already downloaded? If yes, use it for offline capability. If no, use InteractiveServer with SignalR. On subsequent visits, WebAssembly is cached, so the app runs offline.
Use Auto mode when you want both fast first loads and offline capability without deciding upfront. It adapts based on what's available.
@code {
[Parameter] public int Id { get; set; }
private Product product = new();
private bool isFavorite;
protected override async Task OnInitializedAsync()
{
// First render: SSR on server
// After hydration: runs in browser or server
product = await ProductService.GetProductAsync(Id);
isFavorite = await FavoritesService.IsFavoriteAsync(Id);
}
private async Task AddToCart()
{
// Executes where the component is running
await CartService.AddAsync(product);
}
private void ToggleFavorite()
{
isFavorite = !isFavorite;
// Persists to localStorage if WASM, API if Server
}
}
Decision Matrix
SSR: Content-first pages, SEO-critical, no interactivity needed. InteractiveServer: Real-time features, admin tools, internal apps. InteractiveWebAssembly: Offline apps, high interactivity, client-heavy logic. Auto: Default for new projects, adapts automatically.
Project Setup & Architecture
Setting up a hybrid Blazor app requires planning. You need separate projects for server components and WebAssembly components, plus a shared library for models. Get the structure wrong and you'll fight the framework.
Create the Solution
Start with the Blazor Web App template configured for interactive rendering. This gives you the correct project structure with all references wired up.
Terminal
dotnet new blazor -o ProductCatalog -int Auto
cd ProductCatalog
dotnet sln create
# Creates three projects:
# ProductCatalog - Server project (SSR + Interactive Server)
# ProductCatalog.Client - WebAssembly project
# ProductCatalog.Shared - Shared models/contracts
Configure Render Modes in Program.cs
The server project hosts everything. Configure which render modes you'll support. This determines what gets sent to the browser.
Program.cs
using ProductCatalog.Client;
using ProductCatalog.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
// Register your services
builder.Services.AddScoped();
builder.Services.AddScoped();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(ProductCatalog.Client._Imports).Assembly);
app.Run();
Architecture Strategy
Here's how to organize components for maximum reuse and performance:
Server project: SSR pages (product listings, product details, static content)
Components in the server project can use database contexts directly. Components in the client project must call APIs. Shared components work in both but have limited capabilities.
Project References
Server project references Client project and Shared project. Client project references only Shared project. Never reference server code from client code. It won't compile for WebAssembly.
Service Registration Pattern
Services need different implementations for server vs client. Use interfaces to abstract the difference.
Service Registration
// Shared/IProductService.cs
public interface IProductService
{
Task GetProductsAsync();
Task GetProductAsync(int id);
}
// Server/Services/ProductService.cs
public class ProductService : IProductService
{
private readonly AppDbContext _db;
public ProductService(AppDbContext db) => _db = db;
public async Task GetProductsAsync()
{
// Direct database access on server
return await _db.Products.ToArrayAsync();
}
}
// Client/Services/ProductService.cs
public class ProductService : IProductService
{
private readonly HttpClient _http;
public ProductService(HttpClient http) => _http = http;
public async Task GetProductsAsync()
{
// API call from WebAssembly
return await _http.GetFromJsonAsync("/api/products")
?? Array.Empty();
}
}
Streaming SSR Fundamentals
Streaming lets you send HTML in chunks. The browser shows content immediately while your server fetches slower data. Users see something useful in 50ms instead of waiting 2 seconds for everything.
Basic Streaming Pattern
Mark async operations that should stream with the @attribute [StreamRendering(true)] directive in .NET 8.
This enables streaming rendering for the component. Blazor renders the page twice: the first pass sends instant content,
and the second pass patches in the data when the async work completes. In .NET 9 or later, you can omit the
true parameter because it becomes the default.
Don't show "Loading..." text. Show skeleton placeholders that match the content shape. It looks more polished and reduces perceived wait time.
Skeleton Loading
@if (products == null)
{
@for (int i = 0; i < 12; i++)
{
}
}
else
{
@foreach (var product in products)
{
}
}
Progressive Data Loading
Load critical data first, then progressive enhance with secondary data. Product details load instantly. Related products stream in after. Reviews come last.
Progressive Loading
protected override async Task OnInitializedAsync()
{
// Priority 1: Essential data (renders first)
product = await ProductService.GetProductAsync(Id);
StateHasChanged(); // Force render
// Priority 2: Important but not blocking
var relatedTask = ProductService.GetRelatedAsync(Id);
var reviewsTask = ReviewService.GetReviewsAsync(Id);
await Task.WhenAll(relatedTask, reviewsTask);
relatedProducts = await relatedTask;
reviews = await reviewsTask;
// Blazor automatically renders after this
}
Streaming Gotchas
Form submissions don't work during streaming. User input gets lost when the second render happens. Use interactive islands for forms. Streaming also breaks if you have layout-dependent JavaScript that runs before content arrives.
Interactive Islands Pattern
The islands architecture gives you the best of both worlds. Server-render the page for speed and SEO. Add small interactive components where you need them. The page loads fast, search engines see everything, and interactive bits work instantly.
Creating an Interactive Island
Any component can be an island. Add a render mode attribute and it becomes interactive while the rest of the page stays static.
Island Component
@* ProductPage.razor - SSR page *@
@page "/product/{id:int}"
@product.Name
@product.Description
@code {
[Parameter] public int Id { get; set; }
private Product product = new();
protected override async Task OnInitializedAsync()
{
product = await ProductService.GetProductAsync(Id);
}
}
When an interactive island loads, it goes through hydration. The server sends the initial HTML. JavaScript takes over and makes it interactive. This process needs to be fast or users notice lag.
Keep island components small. Big components take longer to hydrate. Split functionality into multiple islands if one gets too large.
Passing Data to Islands
Islands can receive parameters from their SSR parent. But those parameters must be JSON-serializable. No passing database contexts or complex objects.
Data Passing
@* Parent SSR component *@
@code {
// Simple types and arrays serialize fine
private Product product = new();
private Review[] reviews = Array.Empty();
}
@* ProductReviews.razor - Interactive island *@
Reviews for @ProductName
@foreach (var review in currentReviews)
{
}
@code {
[Parameter] public int ProductId { get; set; }
[Parameter] public string ProductName { get; set; } = "";
[Parameter] public Review[] InitialReviews { get; set; } = Array.Empty();
private List currentReviews = new();
private int page = 1;
protected override void OnInitialized()
{
currentReviews.AddRange(InitialReviews);
}
private async Task LoadMore()
{
page++;
var more = await ReviewService.GetPageAsync(ProductId, page);
currentReviews.AddRange(more);
}
}
Islands Best Practices
Use InteractiveServer for islands that need server data access. Use InteractiveWebAssembly for islands that work offline. Avoid mixing modes unnecessarily. Keep islands focused on one task. Multiple small islands perform better than one large island.
Auto Render Mode Deep Dive
Auto mode sounds simple but the mechanics are subtle. Understanding how it decides between Server and WebAssembly helps you build apps that adapt correctly.
How Auto Mode Works
First request: SSR generates initial HTML. After HTML arrives, JavaScript checks if the WebAssembly runtime is downloaded. If yes, hydrate as WebAssembly. If no, connect via SignalR for InteractiveServer. The choice persists in browser storage.
Second visit: WebAssembly is cached, so the app hydrates as WebAssembly immediately. No server round trips for UI updates. The app works offline.
When Auto mode switches from InteractiveServer to WebAssembly on subsequent visits, component state doesn't transfer automatically. You need to persist state explicitly.
State Persistence
@inject IJSRuntime JS
private async Task SaveState()
{
var state = new
{
CartItems = items,
LastUpdated = DateTime.Now
};
await JS.InvokeVoidAsync("localStorage.setItem",
"cart-state",
JsonSerializer.Serialize(state));
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Try to restore state from previous session
var json = await JS.InvokeAsync(
"localStorage.getItem",
"cart-state");
if (!string.IsNullOrEmpty(json))
{
var state = JsonSerializer.Deserialize(json);
items = state?.CartItems;
StateHasChanged();
}
}
}
Fallback Behavior
If WebAssembly fails to download (network issues, corporate firewall), Auto mode falls back to InteractiveServer permanently for that session. Your code doesn't know or care. It works the same either way.
Auto Mode Tradeoffs
Auto mode gives you flexibility but increases complexity. First-time visitors pay InteractiveServer costs. Returning visitors get WebAssembly benefits. If you know your users will only visit once (marketing site), skip Auto and use pure SSR with targeted islands.
Data Fetching Strategies
How you fetch data determines your app's performance. Fetch too early and you block rendering. Fetch too late and users see loading spinners. The right strategy depends on render mode and caching.
SSR Prefetch Pattern
With SSR, fetch all data in OnInitializedAsync before rendering. The server waits for data, renders complete HTML, and sends it. No loading states needed.
SSR Data Fetching
@page "/products"
@foreach (var product in products)
{
}
@code {
private Product[] products = Array.Empty();
protected override async Task OnInitializedAsync()
{
// Blocks until data loads, then renders complete page
products = await ProductService.GetAllAsync();
}
}
Client Hydration with Loading States
Interactive components can't block rendering. They show immediately with skeleton UI, then update when data arrives. This prevents the blank screen problem.
Cache aggressively in WebAssembly. The data lives in the browser, so repeated visits are instant. Use memory cache with expiration for data that changes occasionally.
Client-Side Caching
public class CachedProductService : IProductService
{
private readonly HttpClient _http;
private readonly Dictionary _cache = new();
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);
public CachedProductService(HttpClient http) => _http = http;
public async Task GetAllAsync()
{
const string key = "all-products";
if (_cache.TryGetValue(key, out var cached) &&
cached.Expiry > DateTime.Now)
{
return (Product[])cached.Data;
}
var products = await _http.GetFromJsonAsync("/api/products")
?? Array.Empty();
_cache[key] = (DateTime.Now + _cacheDuration, products);
return products;
}
public async Task GetAsync(int id)
{
var key = $"product-{id}";
if (_cache.TryGetValue(key, out var cached) &&
cached.Expiry > DateTime.Now)
{
return (Product)cached.Data;
}
var product = await _http.GetFromJsonAsync($"/api/products/{id}");
if (product != null)
{
_cache[key] = (DateTime.Now + _cacheDuration, product);
}
return product;
}
public void InvalidateCache()
{
_cache.Clear();
}
}
Caching Gotchas
Cache invalidation is hard. Clear cache when users add items to cart or change data. Don't cache user-specific data in WebAssembly if multiple users share devices. Use session storage instead of memory for sensitive data.
Forms & Validation Across Modes
Forms behave differently in SSR vs interactive modes. SSR forms submit to the server via HTTP POST. Interactive forms use event handlers. Validation works in both, but error handling diverges.
SSR Form Pattern
SSR forms use standard HTML form submission. Blazor enhances them with anti-forgery tokens automatically. Validation runs on the server. Errors return with the re-rendered page.
}
@code {
[SupplyParameterFromForm]
private ContactModel model { get; set; } = new();
private string? successMessage;
private async Task HandleSubmit()
{
// Runs on server, form posts back
await ContactService.SendMessageAsync(model);
successMessage = "Thank you! We'll be in touch soon.";
model = new(); // Reset form
}
public class ContactModel
{
[Required, StringLength(100)]
public string Name { get; set; } = "";
[Required, EmailAddress]
public string Email { get; set; } = "";
[Required, StringLength(1000)]
public string Message { get; set; } = "";
}
}
Interactive Form Pattern
Interactive forms validate and submit without page reloads. You control the entire flow with C# event handlers. Show loading states, handle errors, and update UI immediately.
Put validation attributes on your models in the Shared project. They work identically in SSR and interactive modes. Client-side validation is instant, but always validate on the server too.
Shared Model Validation
// Shared/Models/CheckoutModel.cs
public class CheckoutModel
{
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 2)]
public string FullName { get; set; } = "";
[Required]
[EmailAddress(ErrorMessage = "Invalid email address")]
public string Email { get; set; } = "";
[Required]
[Phone(ErrorMessage = "Invalid phone number")]
public string Phone { get; set; } = "";
[Required]
[StringLength(200, MinimumLength = 5)]
public string Address { get; set; } = "";
[Required]
[RegularExpression(@"^\d{5}$", ErrorMessage = "Invalid ZIP code")]
public string ZipCode { get; set; } = "";
[Required]
[CreditCard(ErrorMessage = "Invalid card number")]
public string CardNumber { get; set; } = "";
[Required]
[Range(1, 12, ErrorMessage = "Month must be between 1 and 12")]
public int ExpiryMonth { get; set; }
[Required]
[Range(2025, 2050, ErrorMessage = "Invalid expiry year")]
public int ExpiryYear { get; set; }
}
Form Security
SSR forms include anti-forgery tokens automatically. Interactive forms don't need them since they use SignalR or run in-browser. Never trust client-side validation alone. Always validate on the server API for interactive forms that call services.
JavaScript Interop in Hybrid Apps
JavaScript interop lets you call browser APIs and third-party libraries from C#. But SSR components can't access JavaScript. They run on the server before the browser exists. Plan your interop calls carefully.
SSR-Safe JavaScript Pattern
Check if JavaScript is available before calling it. In SSR, JS runtime throws exceptions. Wrap all JavaScript calls in OnAfterRenderAsync to ensure they only run in the browser.
SSR-Safe Interop
@inject IJSRuntime JS
private bool isJavaScriptAvailable;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
// This runs in browser, not during SSR
await JS.InvokeVoidAsync("localStorage.setItem", "visited", "true");
isJavaScriptAvailable = true;
StateHasChanged();
}
catch
{
// SSR or JavaScript disabled
isJavaScriptAvailable = false;
}
}
}
private async Task SavePreference(string key, string value)
{
if (!isJavaScriptAvailable) return;
await JS.InvokeVoidAsync("localStorage.setItem", key, value);
}
Conditional Module Loading
Load JavaScript modules only when needed. This reduces initial bundle size and speeds up first render.
Lazy Module Loading
@implements IAsyncDisposable
@inject IJSRuntime JS
private IJSObjectReference? chartModule;
private async Task InitializeChart()
{
// Load module only when user interacts
chartModule ??= await JS.InvokeAsync(
"import",
"./js/chart-component.js");
await chartModule.InvokeVoidAsync("initChart", chartData);
}
public async ValueTask DisposeAsync()
{
if (chartModule != null)
{
await chartModule.DisposeAsync();
}
}
Module Initialization Pattern
Create a JavaScript module with initialization and cleanup functions. Import it lazily and dispose properly.
chart-component.js
// wwwroot/js/chart-component.js
let chartInstance = null;
export function initChart(elementId, data) {
const canvas = document.getElementById(elementId);
if (!canvas) return;
chartInstance = new Chart(canvas, {
type: 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false
}
});
}
export function updateChart(data) {
if (chartInstance) {
chartInstance.data = data;
chartInstance.update();
}
}
export function destroyChart() {
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
}
Interop Performance
JavaScript interop has overhead. Each call crosses the C#/JavaScript boundary. Batch operations when possible. Don't call JavaScript in tight loops. Cache JavaScript object references instead of looking them up repeatedly.
State Management Patterns
State management gets complicated when components run in different modes. SSR components can't share state with interactive components easily. Plan your state architecture upfront.
Cascading Parameters for Shared State
Cascading parameters flow data down the component tree. They work across render modes but have limitations. Changes don't automatically notify children.
InteractiveServer components lose state if the SignalR connection drops. Persist critical state to survive reconnection.
Circuit State Persistence
@inject ProtectedSessionStorage Storage
private CartModel cart = new();
protected override async Task OnInitializedAsync()
{
// Restore state from session storage
var result = await Storage.GetAsync("cart-state");
if (result.Success && result.Value != null)
{
cart = result.Value;
}
}
private async Task UpdateCart()
{
cart.LastModified = DateTime.Now;
// Persist state for circuit resilience
await Storage.SetAsync("cart-state", cart);
}
// Handle reconnection
private async Task OnCircuitReconnected()
{
// Reload critical state
var result = await Storage.GetAsync("cart-state");
if (result.Success && result.Value != null)
{
cart = result.Value;
StateHasChanged();
}
}
State Scoping
Scoped services create one instance per user circuit (InteractiveServer) or per user session (WebAssembly). Singleton services share state across all users. Be careful with singletons in server apps. Use scoped for user-specific state.
Performance Optimization
Performance isn't one number. It's Time to First Byte (TTFB), First Contentful Paint (FCP), Time to Interactive (TTI), and bundle size. Optimize each metric for your use case.
TTFB Optimization
SSR shines here. Server generates HTML fast and sends it immediately. Keep database queries fast, use caching, and avoid synchronous I/O. Target TTFB under 200ms.
Hydration converts static HTML into interactive components. Big components take longer. Split large pages into smaller interactive islands. Only hydrate what needs interactivity.
Minimal Hydration
@* Bad: Entire page interactive *@
@page "/product/{id:int}"
@rendermode InteractiveAuto
@* Good: Only interactive parts *@
@page "/product/{id:int}"
Lazy Loading Components
Don't load WebAssembly components until the user needs them. Use lazy loading with @using directives and load on-demand.
Search engines crawl HTML. If your content needs JavaScript to render, you lose SEO. SSR solves this by sending complete HTML immediately. But you need to handle metadata, structured data, and crawlability correctly.
Dynamic Metadata Management
Set page titles, descriptions, and Open Graph tags dynamically based on content. Use Blazor's HeadContent component to inject metadata into the page head.
@code {
[Parameter] public int Id { get; set; }
private Product product = new();
protected override async Task OnInitializedAsync()
{
product = await ProductService.GetProductAsync(Id);
}
}
Structured Data for Rich Results
Add JSON-LD structured data to help search engines understand your content. Products, reviews, and FAQ pages benefit from rich results in search.
Product Structured Data
@page "/product/{id:int}"
@code {
private string ProductStructuredData =>
JsonSerializer.Serialize(new
{
context = "https://schema.org",
type = "Product",
name = product.Name,
description = product.Description,
image = product.ImageUrl,
offers = new
{
type = "Offer",
price = product.Price,
priceCurrency = "USD",
availability = product.InStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock"
},
aggregateRating = new
{
type = "AggregateRating",
ratingValue = product.AverageRating,
reviewCount = product.ReviewCount
}
}, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
Progressive Enhancement Pattern
Make your site work without JavaScript. Forms submit via HTTP POST. Links navigate with full page loads. Interactive features enhance the experience but aren't required.
Progressive Form
@* Works without JavaScript *@
@* Enhanced version with JavaScript *@
@if (!string.IsNullOrEmpty(message))
{
Use SSR for all public-facing pages. Add structured data for products, articles, and FAQs. Test with Google Search Console and Lighthouse. Ensure canonical URLs are correct. Provide alt text for all images. Use semantic HTML tags.
Error Handling & Resilience
Network failures happen. SignalR connections drop. Database queries timeout. Your app needs to handle errors gracefully and recover automatically when possible.
Error Boundaries
Error boundaries catch exceptions in child components and display fallback UI. This prevents the entire app from crashing when one component fails.
Error Boundary
@* App.razor or Layout *@
@Body
Oops! Something went wrong
We're sorry for the inconvenience. Please try refreshing the page.
InteractiveServer components lose their connection if the network drops. Handle reconnection attempts and notify users when the connection is unstable.
Reconnection Handler
⚠️
Connection lost
Attempting to reconnect...
Circuit Disposal
Server circuits have limited lifetimes. Save critical state before the circuit disposes. Use IAsyncDisposable to clean up resources.
Circuit Cleanup
@implements IAsyncDisposable
@inject ProtectedSessionStorage Storage
private Timer? autosaveTimer;
protected override void OnInitialized()
{
// Autosave every 30 seconds
autosaveTimer = new Timer(async _ =>
{
await SaveState();
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
}
private async Task SaveState()
{
await Storage.SetAsync("form-data", formData);
}
public async ValueTask DisposeAsync()
{
// Save one last time before disposal
await SaveState();
autosaveTimer?.Dispose();
}
Resilience Patterns
Use Polly for retry policies on API calls. Implement circuit breakers for failing services. Cache responses to serve stale data during outages. Log errors to monitoring tools like Application Insights. Test failure scenarios in development.
Production Deployment
Deploying hybrid Blazor apps requires careful configuration. Prerendering, logging, identity integration, and security settings need attention.
Prerendering Configuration
Prerendering generates static HTML at build time for known routes. This eliminates TTFB for those pages. Configure prerendering in Program.cs.
Prerendering Setup
// Program.cs
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
var app = builder.Build();
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(ProductCatalog.Client._Imports).Assembly);
// Prerender specific routes
app.MapStaticAssets();
app.Run();
Production Logging
Configure structured logging for production. Log errors, performance metrics, and user actions to diagnose issues.
Enable response compression. Configure CDN for static assets. Set up health checks. Monitor SignalR connection limits. Configure autoscaling based on CPU and memory. Test failover scenarios. Back up configuration and secrets.
Migration Guide
Migrating from Blazor Server (.NET 7) or Blazor WebAssembly (.NET 7) to the unified model in .NET 8 requires understanding render mode changes.
From Blazor Server (.NET 7)
Blazor Server in .NET 7 was always interactive. In .NET 8, you must explicitly choose render modes. Add @rendermode InteractiveServer to components that need interactivity.
Before (.NET 7)
@* Blazor Server - always interactive *@
@page "/counter"
Blazor WebAssembly in .NET 7 was standalone. In .NET 8, WebAssembly components live in a separate Client project and are hosted by the server project.
Migration Steps
# 1. Create new .NET 8 Blazor Web App
dotnet new blazor -o MyApp -int Auto
# 2. Copy components from .NET 7 WASM project to Client project
cp -r OldApp/Pages/* MyApp.Client/Pages/
cp -r OldApp/Shared/* MyApp.Client/Shared/
# 3. Update Program.cs to register services
# Server/Program.cs
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
# 4. Add render mode to components
@rendermode InteractiveWebAssembly
# 5. Update service registrations
# Client project needs HttpClient, Server project needs DbContext
Breaking Changes
NavigationManager API changes. HttpClient configuration is different. Some component lifecycle methods have new signatures.
API Updates
// .NET 7: NavigationManager.NavigateTo
Nav.NavigateTo("/products", forceLoad: true);
// .NET 8: Same API, but behavior differs with render modes
Nav.NavigateTo("/products", forceLoad: true);
// .NET 7: HttpClient registration (WebAssembly)
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// .NET 8: Separate registration for Client project
// Client/Program.cs
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri("https://api.yoursite.com") });
Migration Strategy
Start with SSR for all pages. Identify components that need interactivity. Add render mode attributes incrementally. Test each component individually. Use Auto mode as default for new features. Gradually refactor to leverage streaming and islands.
Frequently Asked Questions
When should I use SSR vs interactive render modes?
Use SSR for content-heavy pages where SEO and fast initial load matter most. Product listings, blog posts, and landing pages work great with SSR. Add interactive islands only where you need client-side behavior like cart updates, filters, or real-time features. This hybrid approach gives you SEO benefits with selective interactivity.
How does auto render mode decide between Server and WebAssembly?
Auto mode starts with SSR for the first render, then checks if the WebAssembly runtime is downloaded. If available, it switches to WebAssembly for offline capability. If not, it uses InteractiveServer with SignalR. The mode persists across navigation, so once WASM loads, all subsequent interactive components use it.
Can I preserve form state during streaming SSR?
Yes, but you need to handle it carefully. Use anti-forgery tokens and persist form values in hidden fields or local storage. When streaming completes, reinitialize form state from persisted data. For interactive forms, consider using islands with InteractiveServer mode instead of SSR to maintain natural state.
What's the performance cost of hydration?
Hydration adds 50-200ms depending on component complexity and JavaScript bundle size. Interactive Server hydrates faster since it only needs SignalR setup. WebAssembly requires downloading the .NET runtime (about 1.5MB gzipped), which takes 200-500ms on good connections. Use islands strategically to minimize hydration cost.
How do search engines handle streaming SSR content?
Modern search bots wait for full page load, so they'll see your complete streamed content. However, initial HTML snapshots may miss late-streaming sections. For critical SEO content like product details or articles, render it in the first chunk. Use streaming for secondary content like recommendations or reviews.