Mixed Render Modes in Blazor: Stop Making Your Entire App Interactive
For years, the first choice you made in a Blazor project set the course for your entire architecture:
"This is a Blazor Server app."
"This is a Blazor WebAssembly app."
That single binary choice locked down your deployment, your hosting costs, and your state management across every single screen.
Modern Blazor Web Apps removed that constraint. Within a single application, you can mix static server-rendered pages, server-side interactive circuits, and client-side WebAssembly components. You can even host multiple rendering models on the exact same page.
Yet, many teams still build apps as if it's 2020. They slap @rendermode InteractiveServer onto their root Routes component and call it a day.
Making everything interactive by default introduces unnecessary complexity, persistent SignalR circuit overhead, bloated WASM downloads, and elevated server memory usage. A far more practical strategy is to start static by default, then drop in interactivity only where the feature demands it.
Let’s walk through how to architect a mixed-mode page, why the boundaries matter, and where developers usually get tripped up.
The Four Render Modes at a Glance
Before mixing modes, it helps to be precise about what each mode actually provides:
| Mode | Where Code Runs | Interactivity? | Practical Use Case |
| Static SSR | Server | No | Content-heavy pages, marketing, read-only forms, docs. |
| Interactive Server | Server (via SignalR) | Yes | Real-time dashboards, internal line-of-business tools, heavy DB access. |
| Interactive WebAssembly | Browser (via WASM) | Yes | Offline tools, high-frequency local UI updates, client-side calculators. |
| Interactive Auto | Server initially, WASM later | Yes | General public app screens where immediate startup and offline/client capability matter. |
The mistake most teams make isn't choosing the wrong mode for the app—it’s assuming the choice has to apply to the whole app at all.
The Default Fallacy: Global Interactivity
In a standard template, it's tempting to wire up App.razor like this:
<Routes @rendermode="InteractiveServer" />
This trickles InteractiveServer down to every single route in the system.
Application
├── Home ───> Interactive Server (Why?)
├── About ───> Interactive Server (Why?)
├── Products ───> Interactive Server
├── Privacy ───> Interactive Server (Why?)
└── Dashboard ───> Interactive Server
Ask yourself: Does an About page need an open WebSocket circuit holding state on your web server? Does a static privacy policy need real-time event handling?
Of course not.
The "Islands of Interactivity" Architecture
Consider a standard e-commerce product page (/products/42).
Most of this page—the title, description, spec tables, documentation links, and customer reviews—is pure document content. It never changes after the initial HTTP GET request.
Only two small regions need live behavior: an Add to Cart widget (which needs to talk to server state) and an interactive Price Calculator (which runs local math as the user toggles options).
Instead of turning the entire page into a live app, build a static document that embeds small interactive islands:
Product Page (/products/42)
│
├── [Static SSR] Product Title & Description
├── [Static SSR] Specifications & Reviews
│
├── [Interactive Server] PurchasePanel Component
│ └── Validates stock, modifies session, writes to server cart
│
└── [Interactive WASM] PriceCalculator Component
└── Instant client-side calculations as sliders move
Step-by-Step: Building a Mixed-Mode Product Page
Let's assemble this exact page.
1. Configure Services in Program.cs
First, tell ASP.NET Core that the host application supports both interactive paradigms:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode();
app.Run();
2. Build the Static Shell (Product.razor)
In modern Blazor, Static SSR is the default. If you omit @rendermode on a page or component, it simply renders static HTML on the server and returns it to the client. Zero circuit overhead, zero WASM downloads.
@page "/products/{id:int}"
<PageTitle>@product.Name</PageTitle>
<article class="product-page">
<h1>@product.Name</h1>
<p class="lead">@product.Description</p>
<section>
<h2>Specifications</h2>
<ul>
<li>Mechanical Switches</li>
<li>USB-C Passthrough</li>
<li>RGB Backlighting</li>
</ul>
</section>
<!-- Interactive Island 1: Server -->
<section class="my-4">
<PurchasePanel Price="@product.Price" @rendermode="InteractiveServer" />
</section>
<!-- Interactive Island 2: WebAssembly -->
<section class="my-4">
<PriceCalculator AnnualPrice="@product.Price" @rendermode="InteractiveWebAssembly" />
</section>
</article>
@code {
[Parameter] public int Id { get; set; }
private Product product = new(
42,
"Developer Mechanical Keyboard",
149.99m,
"An ergonomic, fully programmable mechanical keyboard built for heavy workflows."
);
}
3. Create the Server-Interactive Island (PurchasePanel.razor)
This component lives on the server project, handling user input, quantity adjustments, and server-side cart calls via SignalR:
<div class="card p-3">
<h3>Purchase Options</h3>
<div class="mb-3">
<label for="qty">Quantity:</label>
<input id="qty" type="number" min="1" @bind="quantity" class="form-control" />
</div>
<p>Total: <strong>@Total.ToString("C")</strong></p>
<button class="btn btn-primary" @onclick="AddToCart">
Add to Cart
</button>
@if (!string.IsNullOrEmpty(statusMessage))
{
<div class="alert alert-info mt-2">@statusMessage</div>
}
</div>
@code {
[Parameter] public decimal Price { get; set; }
private int quantity = 1;
private string? statusMessage;
private decimal Total => Price * quantity;
private void AddToCart()
{
// Real server-side operation (e.g., DB check, session update)
statusMessage = $"Added {quantity} item(s) to your server cart.";
}
}
4. Create the WebAssembly Island (PriceCalculator.razor)
Because WebAssembly components execute inside the user's browser, this component must live in your dedicated .Client project.
<div class="card p-3 bg-light">
<h3>Enterprise Volume Calculator</h3>
<p class="text-muted">Runs 100% client-side via WebAssembly</p>
<div class="row">
<div class="col">
<label>Seats:</label>
<input type="number" min="1" @bind="seats" @bind:event="oninput" class="form-control" />
</div>
<div class="col">
<label>Years:</label>
<input type="number" min="1" @bind="years" @bind:event="oninput" class="form-control" />
</div>
</div>
<h4 class="mt-3">Estimated Quote: @CalculatedTotal.ToString("C")</h4>
</div>
@code {
[Parameter] public decimal AnnualPrice { get; set; }
private int seats = 10;
private int years = 1;
private decimal CalculatedTotal
{
get
{
var raw = AnnualPrice * seats * years;
var discount = seats > 25 ? 0.15m : 0.0m; // Local math logic
return raw * (1 - discount);
}
}
}
Core Rules for Mixed Render Modes
When you start mixing modes within the same app, you need to be aware of a few structural boundaries.
Rule 1: Render Modes Propagate Downward
If a parent component declares @rendermode InteractiveServer, every child component rendered inside it becomes part of that same InteractiveServer boundary.
[InteractiveServer Parent]
└── [Child Component] <-- Automatically InteractiveServer
Rule 2: You Cannot Nest Different Interactive Modes
You cannot place a InteractiveWebAssembly component inside an InteractiveServer parent.
If you want sibling components to use different interactive modes, their common parent must be Static SSR:
[Static SSR Parent]
├── [InteractiveServer Child] <-- VALID
└── [InteractiveWASM Child] <-- VALID
Rule 3: Parameter Serialization Across Boundaries
When a Static SSR parent passes parameters to an interactive child, those values cross a boundary (rendered HTML to circuit/WASM state).
Primitive Types & DTOs: Strings, numbers, records, and simple serializable POCOs work seamlessly.
RenderFragments & Delegates: You cannot pass a
<RenderFragment>or a C#Action/Funcdelegate across a static-to-interactive boundary. The framework cannot JSON-serialize raw C# execution logic.
Architectural Checklist: How to Choose a Mode
When designing a new component, run down this decision tree:
Does the component need user event handling (@onclick, @bind)?
│
├── NO ──> Use Static SSR (Default)
│
└── YES ──> Does it need direct access to server-only resources (DB, internal APIs)?
│
├── YES ──> Use InteractiveServer
│
└── NO ──> Is it heavy on UI math, local calculations, or client responsiveness?
│
├── YES ──> Use InteractiveWebAssembly
│
└── UNSURE ──> Use InteractiveAuto (Server start, WASM background download)
A Practical Rule of Thumb
Don't reach for Interactive Auto assuming it's an "easy default." Auto requires your component services to be fully abstract—capable of running both direct DB calls on the server and HttpClient calls in WASM. That architectural abstraction is great for large systems, but it adds structural overhead you might not need for simple tools.
Summary
The browser is exceptionally good at parsing and displaying HTML. Let it do what it was designed to do.
By keeping your page shells static and surgically injecting interactivity where it matters, you get the best of all worlds: lightning-fast initial renders, minimal server memory footprints, zero client bundle bloat for static content, and rich client/server interactivity right where your application actually demands it.

Comments
Post a Comment