Speed Without the Weight
When users hit a blog post, product listing, or documentation page, they expect content within 100 milliseconds. Every second you make them wait for JavaScript bundles to download, parse, and execute is a second they might bounce. Traditional SPAs force users to download hundreds of kilobytes before showing anything meaningful.
Blazor's static server-side rendering (SSR) skips all that. Your component runs once on the server during the HTTP request, generates pure HTML, and sends it straight to the browser. No JavaScript framework required, no WebAssembly download, no SignalR connection. Users see content instantly because the HTML is ready when the response arrives.
You'll learn how SSR works under the hood, when it outperforms interactive modes, and how to build pages that leverage server rendering for maximum speed. By the end, you'll understand why SSR should be your default choice for content-heavy pages.
The Rendering Pipeline
When a browser requests an SSR page, the server executes your Razor component's lifecycle methods immediately. The component calls your data access layer, loads from databases or APIs, then renders its markup using that data. Blazor converts the Razor syntax into HTML and sends the complete page as the HTTP response.
This all happens synchronously during the request. There's no separate rendering phase, no hydration step, and no client-side initialization. The browser receives ready-to-display HTML and shows it instantly. Users can read, scroll, and click links without waiting for any JavaScript to load.
Traditional Blazor modes (Server, WebAssembly, Auto) require establishing connections or downloading runtimes before they can show anything. SSR eliminates this startup cost entirely, making it the fastest option for initial page loads.
@page "/blog/{slug}"
@inject IPostRepository PostRepo
<PageTitle>@post?.Title ?? "Loading..."</PageTitle>
@if (post == null)
{
<p>Post not found</p>
}
else
{
<article>
<h1>@post.Title</h1>
<p class="meta">
By @post.Author | @post.PublishedDate.ToShortDateString()
</p>
<div class="content">
@((MarkupString)post.HtmlContent)
</div>
</article>
}
@code {
[Parameter]
public string Slug { get; set; } = "";
private BlogPost? post;
protected override async Task OnInitializedAsync()
{
// Runs on server during request
// Direct database access, no API needed
post = await PostRepo.GetBySlugAsync(Slug);
}
}
This component runs entirely on the server. When someone visits /blog/my-post, Blazor executes OnInitializedAsync, queries the database through the repository, and renders the article HTML. The browser receives a complete page with the title, author, date, and content already filled in. No loading spinner, no flash of empty content.
Server-Side Data Loading
SSR components have full access to server resources during rendering. You can inject Entity Framework contexts, file system services, configuration providers, or any other server-side dependency. This is fundamentally different from client-side rendering where you're limited to HTTP calls.
Load data in OnInitializedAsync or OnParametersSetAsync, and it's available immediately when the component renders. There's no need to create API endpoints for every piece of data. Query your database directly, read configuration files, or call internal services that aren't exposed to the internet.
@page "/products"
@inject ApplicationDbContext Db
@inject IConfiguration Config
<PageTitle>Products</PageTitle>
<h1>Product Catalog</h1>
<div class="featured-banner" style="background: @featuredBgColor;">
<p>@featuredMessage</p>
</div>
<div class="product-grid">
@foreach (var product in products)
{
<div class="product-card">
<img src="@product.ImageUrl" alt="@product.Name" />
<h3>@product.Name</h3>
<p>@product.Price.ToString("C")</p>
<p>@product.StockCount units in stock</p>
</div>
}
</div>
@code {
private List<Product> products = new();
private string featuredMessage = "";
private string featuredBgColor = "";
protected override async Task OnInitializedAsync()
{
// Direct database query - no API layer needed
products = await Db.Products
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.Take(20)
.ToListAsync();
// Access server configuration directly
featuredMessage = Config["Marketing:FeaturedMessage"]
?? "Check out our products!";
featuredBgColor = Config["Marketing:BannerColor"] ?? "#f0f0f0";
}
}
The component queries the database with Entity Framework and reads app configuration, both of which only work server-side. The rendered HTML includes all product details and the configured banner message. Users see the complete page without making separate API calls or waiting for JavaScript to initialize.
Enhanced Navigation Between Pages
Blazor SSR includes enhanced navigation that intercepts link clicks and fetches new pages via JavaScript. When users click a link to another SSR page, Blazor requests the new page content, updates the DOM, and updates the browser history without a full page reload. This gives SPA-like navigation speed while keeping individual pages as fast SSR responses.
Enhanced navigation is progressive enhancement. If JavaScript isn't available or hasn't loaded yet, links work normally with standard full-page navigation. The site remains fully functional without JavaScript, but users with JavaScript enabled get a faster experience.
@page "/"
<PageTitle>Home</PageTitle>
<h1>Welcome to Our Site</h1>
<nav>
<ul>
<li><a href="/products">Products</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<p>
These links use enhanced navigation. When clicked, Blazor fetches
the new page and updates the DOM without a full reload.
</p>
<p>
Open DevTools Network tab and click a link. You'll see an XHR
request instead of a full page navigation. The page updates
smoothly without losing scroll position or resetting focus.
</p>
Standard anchor tags automatically participate in enhanced navigation. Blazor's JavaScript intercepts clicks, fetches the target page, and swaps the content. Users get instant navigation between pages without the download cost of a full SPA framework. If you want to opt out for specific links, add data-enhance-nav="false" to the anchor tag.
Streaming Rendering for Slow Data
When your page loads data from slow sources, you can use streaming rendering to show content progressively. The component renders immediately with placeholders, then streams updated HTML as data becomes available. Users see the page structure instantly instead of staring at a blank screen while data loads.
Wrap slow-loading sections in a <StreamingContent> component or use the @attribute [StreamRendering(true)] directive. Blazor sends the initial render immediately, then pushes HTML updates as your async operations complete. This technique dramatically improves perceived performance for data-heavy pages.
@page "/dashboard"
@attribute [StreamRendering(true)]
@inject IAnalyticsService Analytics
<PageTitle>Dashboard</PageTitle>
<h1>Analytics Dashboard</h1>
<div class="stats-grid">
@if (stats == null)
{
<p>Loading statistics...</p>
}
else
{
<div class="stat-card">
<h3>Total Users</h3>
<p class="stat-value">@stats.TotalUsers.ToString("N0")</p>
</div>
<div class="stat-card">
<h3>Revenue</h3>
<p class="stat-value">@stats.Revenue.ToString("C")</p>
</div>
<div class="stat-card">
<h3>Active Sessions</h3>
<p class="stat-value">@stats.ActiveSessions</p>
</div>
}
</div>
@code {
private AnalyticsStats? stats;
protected override async Task OnInitializedAsync()
{
// This might take 2-3 seconds
stats = await Analytics.GetCurrentStatsAsync();
}
}
With streaming enabled, users see the heading and "Loading statistics..." message within 50ms. Three seconds later when the analytics data arrives, Blazor streams the updated stats to the browser and replaces the loading message. This keeps users engaged instead of showing a blank page during slow data loads.
Build Your First SSR Page
Create a simple product listing page using static SSR to see the performance benefits.
Steps
- Initialize:
dotnet new blazor -n SsrDemo
- Enter directory:
cd SsrDemo
- Add the Products page below
- Run:
dotnet run
- Visit
https://localhost:5001/products
- View page source to see pre-rendered HTML
- Check Network tab - no WebSocket or WASM downloads
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
@page "/products"
<PageTitle>Products - SSR Demo</PageTitle>
<h1>Product Catalog (Static SSR)</h1>
<p>This page renders once on the server. View source to see complete HTML.</p>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;">
@foreach (var product in products)
{
<div style="border: 1px solid #ddd; padding: 15px; border-radius: 8px;">
<h3>@product.Name</h3>
<p><strong>@product.Price.ToString("C")</strong></p>
<p>@product.Description</p>
<p><small>SKU: @product.Sku</small></p>
</div>
}
</div>
@code {
private List<Product> products = new();
protected override void OnInitialized()
{
// Simulates database load - happens on server during request
products = new List<Product>
{
new() { Name = "Wireless Mouse", Price = 29.99m,
Sku = "MS-001", Description = "Ergonomic design" },
new() { Name = "Mechanical Keyboard", Price = 89.99m,
Sku = "KB-002", Description = "Cherry MX switches" },
new() { Name = "USB-C Hub", Price = 49.99m,
Sku = "HB-003", Description = "7-port expansion" },
new() { Name = "Laptop Stand", Price = 39.99m,
Sku = "LS-004", Description = "Adjustable height" },
new() { Name = "Webcam HD", Price = 79.99m,
Sku = "WC-005", Description = "1080p 60fps" },
new() { Name = "Headset", Price = 119.99m,
Sku = "HS-006", Description = "Noise cancelling" }
};
}
private class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public string Sku { get; set; } = "";
public string Description { get; set; } = "";
}
}
Output
The page loads instantly with all six products visible. View the page source and you'll see the complete HTML with product names, prices, and descriptions already rendered. There's no "loading" state and no JavaScript required. The Network tab shows one HTML request with no WebSocket connections or WebAssembly downloads.
Knowing the Limits
SSR can't handle clicks or form inputs without page reloads. If your page needs buttons that toggle visibility, sliders that update values, or any client-side interactivity, pure SSR won't work. Use the interactive islands pattern instead: keep the page in SSR mode and add specific interactive components where needed.
Real-time updates require interactive modes. Chat applications, live dashboards, and collaborative editors need persistent connections to push updates. SSR only renders during the initial request, so it can't receive server-pushed updates. Choose Server mode for SignalR-based real-time features or WebAssembly with a separate WebSocket connection.
Client-side routing needs JavaScript. While enhanced navigation works for most scenarios, complex routing patterns, route guards, or programmatic navigation require interactive components. If your app relies heavily on client-side routing logic, consider Server or WebAssembly modes for those sections.
Use SSR by default, add interactivity selectively. Start every page with static SSR. Only switch specific components to interactive modes when they genuinely need to respond to user input or show real-time updates. This approach gives you the fastest possible page loads while keeping interactive features smooth.