SSR or Interactive? Picking the Right Blend per Page

The Rendering Trade-off

Every component you make interactive adds weight to your page. SignalR connections for Server mode, WebAssembly downloads for client-side rendering, and the JavaScript needed to wire everything up. Meanwhile, static SSR delivers instant page loads with minimal overhead. The question isn't which approach is better, it's which sections of your page actually need interactivity.

Most pages don't need to be fully interactive. Headers, footers, article content, and product descriptions work perfectly as static HTML. Save interactivity for the parts users actually click: search boxes, filters, shopping carts, and form inputs. This focused approach gives you fast page loads while keeping the interactive parts responsive.

You'll learn a practical framework for deciding what to render statically versus interactively. By the end, you'll know how to audit your pages, identify the right blend for different page types, and build hybrid pages that feel fast from first paint to final interaction.

Start with Static, Add Interactivity

Build every page with static SSR first. Load your data on the server, render complete HTML, and send it to the browser. This gives you the fastest possible First Contentful Paint because there's no JavaScript to download, parse, or execute before content appears.

Once the basic page works, identify components that genuinely need to respond to user input. A search box needs to update results as users type. A shopping cart needs to add items without page reload. An accordion needs to collapse and expand sections. These are candidates for interactive rendering.

Everything else can stay static. Navigation menus, breadcrumbs, article text, images, and footer links don't benefit from interactive rendering. They load faster and work better as pure HTML.

Components/Pages/ProductDetail.razor
@page "/product/{id:int}"
@inject IProductService Products

@* Page itself uses static SSR for fast load *@

<PageTitle>@product?.Name ?? "Product"</PageTitle>

@if (product == null)
{
    <p>Product not found</p>
}
else
{
    @* Static content: loads instantly, no JS needed *@
    <div class="product-header">
        <h1>@product.Name</h1>
        <p class="price">@product.Price.ToString("C")</p>
    </div>

    <div class="product-layout">
        <div class="product-images">
            <img src="@product.MainImageUrl" alt="@product.Name" />
        </div>

        <div class="product-details">
            <h2>Description</h2>
            <p>@product.Description</p>

            <h2>Specifications</h2>
            <ul>
                @foreach (var spec in product.Specifications)
                {
                    <li><strong>@spec.Key:</strong> @spec.Value</li>
                }
            </ul>

            @* Interactive component: needs to update cart *@
            <AddToCartButton ProductId="@product.Id"
                            ProductName="@product.Name"
                            Price="@product.Price" />
        </div>
    </div>

    @* Static reviews: just displays, no interaction needed *@
    <section class="reviews">
        <h2>Customer Reviews</h2>
        @foreach (var review in product.Reviews)
        {
            <div class="review">
                <p><strong>@review.AuthorName</strong> - @review.Rating/5</p>
                <p>@review.Comment</p>
            </div>
        }
    </section>
}

@code {
    [Parameter]
    public int Id { get; set; }

    private ProductDetail? product;

    protected override async Task OnInitializedAsync()
    {
        product = await Products.GetDetailByIdAsync(Id);
    }
}

The page loads product data server-side and renders everything statically except the AddToCartButton. Users see the product name, price, images, description, specs, and reviews within 100ms. Only the cart button needs interactivity, so only that component uses an interactive render mode. This keeps the page fast while providing the functionality users expect.

Targeted Interactive Components

Interactive components should be small and focused. Create a separate component for each interactive feature rather than making entire sections interactive. This minimizes the overhead while providing precise control over user actions.

When you extract interactive functionality into focused components, you can easily switch between Server and WebAssembly mode based on performance needs. A simple button works fine in Server mode, while a complex widget might need WebAssembly for instant feedback.

Components/AddToCartButton.razor
@rendermode RenderMode.InteractiveServer
@inject ICartService Cart
@inject NavigationManager Nav

<div class="add-to-cart">
    <label>
        Quantity:
        <input type="number" @bind="quantity" min="1" max="10" />
    </label>

    <button @onclick="AddToCart" disabled="@isAdding">
        @if (isAdding)
        {
            <span>Adding...</span>
        }
        else if (added)
        {
            <span>✓ Added!</span>
        }
        else
        {
            <span>Add to Cart</span>
        }
    </button>
</div>

@code {
    [Parameter, EditorRequired]
    public int ProductId { get; set; }

    [Parameter, EditorRequired]
    public string ProductName { get; set; } = "";

    [Parameter, EditorRequired]
    public decimal Price { get; set; }

    private int quantity = 1;
    private bool isAdding = false;
    private bool added = false;

    private async Task AddToCart()
    {
        isAdding = true;
        added = false;

        try
        {
            await Cart.AddItemAsync(ProductId, ProductName, Price, quantity);
            added = true;

            // Reset after showing success
            await Task.Delay(2000);
            added = false;
        }
        finally
        {
            isAdding = false;
        }
    }
}

This focused component handles cart operations while the rest of the page stays static. Users can change quantity and add items without reloading the page. The component shows loading and success states to provide feedback. Server mode works fine here because adding to cart is an occasional action where 50-100ms latency is acceptable.

The Decision Framework

Ask three questions for every component: Does it display data or accept input? How frequently will users interact with it? Does it need instant feedback or can it tolerate network latency?

Components that only display data should use static SSR. Headers, footers, article content, product listings, and informational sections load faster as static HTML. There's no benefit to making them interactive.

Components that accept occasional input can use Server mode. Forms, search boxes, filters, and settings panels work well with the slight delay from server round trips. Users expect a brief pause when submitting forms or applying filters.

Components needing instant feedback require WebAssembly mode. Drawing tools, rich text editors, calculators, and real-time visualizations feel sluggish with network latency. These justify the WebAssembly download because users interact frequently and expect immediate response.

Decision Matrix Example
// STATIC SSR - Display only, no interaction
// - Navigation menus
// - Blog post content
// - Product descriptions
// - Footer links
// - Breadcrumbs
// - Static images

// SERVER MODE - Occasional input, latency acceptable
// - Search forms (submit on enter)
// - Filters and sorting
// - Login/registration forms
// - Settings panels
// - Comment submission
// - Contact forms

// WEBASSEMBLY MODE - Frequent input, needs instant feedback
// - Rich text editor
// - Drawing canvas
// - Real-time calculator
// - Drag-and-drop organizer
// - Live chart with hover tooltips
// - Spreadsheet-like data grid

// Example: Product listing page
@page "/products"
@inject IProductService Products

<!-- Static: Fast load, no interaction needed -->
<h1>Our Products</h1>
<p>Browse our catalog of @productCount items</p>

<!-- Server mode: Occasional filtering -->
<ProductFilter @rendermode="RenderMode.InteractiveServer"
              OnFilterChanged="HandleFilter" />

<!-- Static: Just displays filtered results -->
<div class="product-grid">
    @foreach (var product in products)
    {
        <ProductCard Product="@product" />
    }
</div>

@code {
    private List<Product> products = new();
    private int productCount = 0;

    protected override async Task OnInitializedAsync()
    {
        products = await Products.GetAllAsync();
        productCount = products.Count;
    }

    private async Task HandleFilter(FilterCriteria criteria)
    {
        products = await Products.GetFilteredAsync(criteria);
    }
}

The page structure puts static content first for instant visibility. The interactive filter component uses Server mode because filtering happens occasionally and users can tolerate a brief wait. The product grid stays static because it just displays the filtered results without needing user interaction.

Common Page Patterns

Different page types have different rendering needs. Content pages like blogs and documentation should be 100% static SSR for maximum speed and SEO. List pages with filtering need a small interactive component for the filters while keeping the list static. Detail pages need minimal interactivity, usually just an action button or two.

Form-heavy pages benefit from Server mode for form handling while keeping explanatory text static. Dashboard pages mix static headers with interactive charts or data tables. Admin pages often need more interactivity but should still render navigation and layout statically.

Components/Pages/BlogPost.razor
@page "/blog/{slug}"
@inject IBlogService Blog

@* 100% static for maximum speed and SEO *@

<PageTitle>@post?.Title ?? "Blog"</PageTitle>

@if (post != null)
{
    <article>
        <header>
            <h1>@post.Title</h1>
            <p class="meta">
                By @post.Author | @post.PublishedDate.ToLongDateString()
            </p>
        </header>

        <div class="content">
            @((MarkupString)post.HtmlContent)
        </div>

        <footer>
            <p>Tags: @string.Join(", ", post.Tags)</p>
        </footer>
    </article>

    <nav class="related-posts">
        <h2>Related Articles</h2>
        <ul>
            @foreach (var related in post.RelatedPosts)
            {
                <li><a href="/blog/@related.Slug">@related.Title</a></li>
            }
        </ul>
    </nav>
}

@code {
    [Parameter]
    public string Slug { get; set; } = "";

    private BlogPost? post;

    protected override async Task OnInitializedAsync()
    {
        post = await Blog.GetBySlugAsync(Slug);
    }
}

Blog posts need zero interactivity beyond clicking links. Keeping everything static gives you sub-100ms page loads, perfect Core Web Vitals scores, and complete HTML for search engines to index. This is the ideal pattern for content-focused pages.

Build a Hybrid Page

Create a product page that mixes static content with an interactive cart button.

Steps

  1. Scaffold: dotnet new blazor -n HybridDemo
  2. Navigate: cd HybridDemo
  3. Add the components below
  4. Run: dotnet run
  5. Visit: https://localhost:5001/product
  6. View page source to see pre-rendered content
  7. Use Network tab to see only the button establishes SignalR
HybridDemo.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
Components/Pages/ProductPage.razor
@page "/product"

<PageTitle>Wireless Mouse - HybridDemo</PageTitle>

<!-- Static content: loads instantly -->
<div style="max-width: 800px; margin: 0 auto; padding: 20px;">
    <h1>Wireless Mouse</h1>
    <p class="price" style="font-size: 24px; color: #007bff;">$29.99</p>

    <img src="https://via.placeholder.com/400x300" alt="Wireless Mouse"
         style="width: 100%; max-width: 400px;" />

    <h2>Description</h2>
    <p>
        Ergonomic wireless mouse with precision tracking and long battery life.
        Perfect for office work or gaming.
    </p>

    <h2>Features</h2>
    <ul>
        <li>2.4GHz wireless connection</li>
        <li>1600 DPI optical sensor</li>
        <li>6-month battery life</li>
        <li>Comfortable ergonomic design</li>
    </ul>

    <!-- Interactive component: only this needs SignalR -->
    <CartButton />
</div>

@* Create Components/CartButton.razor with this content:
@rendermode RenderMode.InteractiveServer

<div style="margin-top: 30px;">
    <button @onclick="AddToCart"
            style="padding: 15px 30px; font-size: 18px; background: #28a745;
                   color: white; border: none; border-radius: 5px; cursor: pointer;">
        @buttonText
    </button>
    @if (itemCount > 0)
    {
        <p style="margin-top: 10px;">Items in cart: @itemCount</p>
    }
</div>

@code {
    private int itemCount = 0;
    private string buttonText = "Add to Cart";

    private async Task AddToCart()
    {
        buttonText = "Adding...";
        await Task.Delay(500); // Simulate API call

        itemCount++;
        buttonText = "Add to Cart";
    }
}
*@

Run result

The page loads instantly with all product information visible. View source shows complete HTML including the title, price, image, and features. Only the cart button establishes a SignalR connection. Click the button and watch the Network tab: you'll see SignalR messages but the rest of the page stays static.

Comparing Approaches

Choose fully static pages when you need maximum performance and SEO. Content sites, marketing pages, and documentation benefit most from pure SSR. You get instant loads, minimal server resources, and perfect search engine visibility.

Choose fully interactive pages when the entire page needs to respond to user input. Admin dashboards, data entry applications, and real-time collaboration tools often need widespread interactivity. Just accept the initial load cost and focus on responsiveness after startup.

Choose hybrid pages for most real-world applications. Keep navigation, headers, footers, and content static for fast loads. Add interactive components for search, filters, forms, and action buttons. This balanced approach delivers great performance while providing the interactivity users expect.

Measure the impact of your choices. Use browser DevTools to check First Contentful Paint, Time to Interactive, and Total Blocking Time. If adding interactive components delays content visibility beyond 500ms, reconsider your approach. The goal is to show content fast while making the interactive parts feel instant once loaded.

How do I...?

Should I default to SSR or interactive components?

Default to static SSR for everything. Only make components interactive when they genuinely need to respond to user input. This approach gives you the fastest page loads. Add Server or WebAssembly mode component by component as requirements demand it.

Can SSR pages include forms and buttons?

Yes, SSR supports forms through standard HTTP POST submissions. Use EditForm with enhanced form handling for better UX. Buttons can trigger navigation or form submissions. For buttons needing instant UI updates without navigation, use interactive components instead.

How do I measure if interactive mode is worth it?

Compare page load time before and after adding interactive components. Use browser DevTools to measure First Contentful Paint and Time to Interactive. If interactive mode delays content visibility by more than 500ms, consider keeping more content in SSR with targeted interactive islands.

What about SEO with interactive components?

Search engines see SSR content immediately but may miss content inside interactive components that render client-side. Put SEO-critical content (headings, product details, articles) in SSR sections. Use interactive components for UI controls, filters, and features that don't need indexing.

Back to Articles