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:

ModeWhere Code RunsInteractivity?Practical Use Case
Static SSRServerNoContent-heavy pages, marketing, read-only forms, docs.
Interactive ServerServer (via SignalR)YesReal-time dashboards, internal line-of-business tools, heavy DB access.
Interactive WebAssemblyBrowser (via WASM)YesOffline tools, high-frequency local UI updates, client-side calculators.
Interactive AutoServer initially, WASM laterYesGeneral 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:

Razor CSHTML
<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:

C#

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.

Razor CSHTML

@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:

Razor CSHTML

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

Razor CSHTML

<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/Func delegate 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

Popular posts from this blog

Customizing PWA Manifest and Icons for a Polished User Experience 🚀

Offline-First Strategy with Blazor PWAs: A Complete Guide 🚀

Yes, Blazor Server can scale!