Published

- 19 min read

Modern Web UIs with Blazor in 2025: State Management & Component Libraries

img of Modern Web UIs with Blazor in 2025: State Management & Component Libraries

Building Modern Web UIs with Blazor in 2025: State Management, Component Libraries, and Performance Optimization

As we move through 2025, Microsoft’s Blazor framework has evolved into a mature and powerful platform for building modern web applications. This article explores the latest advancements in Blazor development, focusing on three critical aspects: state management strategies, component libraries (with special attention to MudBlazor and Radzen), and performance optimization techniques that help Blazor applications run smoothly in production environments.

The State of Blazor in 2025

Blazor has come a long way since its introduction, and in 2025, it stands as a compelling alternative to JavaScript frameworks for building web applications. With the release of .NET 10, Blazor has gained several key enhancements:

  • Improved Rendering Engine: The rendering engine has been optimized to handle more complex UI updates with less overhead.
  • Enhanced Reconnection UI: The Blazor Web App template now includes a built-in ReconnectModal component for improved handling of dropped connections.
  • Better Navigation: Smoother navigation that avoids full-page flickers when moving between pages.
  • Reduced Memory Footprint: Significant improvements in memory management for better performance in resource-constrained environments.
  • Expanded Component Ecosystem: A rich ecosystem of both first-party and third-party component libraries.

These improvements have positioned Blazor as a robust framework for developing modern web applications using C# and .NET instead of JavaScript.

Blazor Rendering Models

Before diving into state management and component libraries, it’s essential to understand the different rendering models available in Blazor as of 2025, as they significantly impact how you approach state management:

Server Rendering

Blazor Server executes your components on the server and maintains a real-time connection with the client using SignalR. Updates are sent to the browser over this connection.

   // In Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

WebAssembly Rendering

Blazor WebAssembly runs your components directly in the browser using WebAssembly, offering a true client-side single-page application (SPA) experience.

   // In Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents();

Auto Render Mode

Introduced in .NET 8 and enhanced in .NET 10, the Auto render mode combines both approaches, initially using server-side rendering for fast startup and then transitioning to WebAssembly once the WASM runtime is downloaded.

   // In App.razor
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>

// In MainLayout.razor
@inherits LayoutComponentBase
@rendermode InteractiveAuto

This hybrid model provides the best of both worlds: fast initial rendering with server-side execution, followed by client-side execution after the WebAssembly runtime is loaded.

State Management Strategies

State management is a crucial aspect of building complex web applications. In 2025, Blazor offers several approaches to state management, from simple to sophisticated:

1. Component State

The simplest form of state management in Blazor is component-local state. Each component maintains its own state, which is lost when the component is unmounted.

   @page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

2. Cascading Parameters

For parent-child component communication, Blazor provides cascading parameters, which allow parent components to pass data down to all nested components.

   // In parent component
<CascadingValue Value="@themeState">
    <ChildComponent />
</CascadingValue>

@code {
    private ThemeState themeState = new ThemeState { IsDarkMode = true };
}

// In child component
@code {
    [CascadingParameter]
    private ThemeState ThemeState { get; set; }
}

public class ThemeState
{
    public bool IsDarkMode { get; set; }
}

3. Service-Based State Management

For more complex applications, a dependency injection-based approach using services is a popular choice:

   // Create a state service
public class CounterState
{
    private int _count = 0;
    public int Count => _count;

    public event Action? OnStateChanged;

    public void IncrementCount()
    {
        _count++;
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnStateChanged?.Invoke();
}
   // Register it as a singleton in Program.cs
builder.Services.AddSingleton<CounterState>();
   // Use it in a component
@page "/counter"
@inject CounterState State
@implements IDisposable

<h1>Counter</h1>

<p>Current count: @State.Count</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    protected override void OnInitialized()
    {
        State.OnStateChanged += StateHasChanged;
    }

    private void IncrementCount()
    {
        State.IncrementCount();
    }

    public void Dispose()
    {
        State.OnStateChanged -= StateHasChanged;
    }
}

4. Fluxor for Unidirectional Data Flow

For larger applications, Fluxor has become a popular state management library that implements the Redux pattern for Blazor. It provides a unidirectional data flow that makes state changes predictable and easier to debug.

First, install the Fluxor NuGet package:

   dotnet add package Fluxor
dotnet add package Fluxor.Blazor.Web
dotnet add package Fluxor.Blazor.Web.ReduxDevTools

Here’s a basic implementation:

   // 1. Define your state
public record CounterState
{
    public int CurrentCount { get; init; }
}

// 2. Define features with initial state
public class CounterFeature : Feature<CounterState>
{
    public override string GetName() => "Counter";
    
    protected override CounterState GetInitialState() => 
        new CounterState { CurrentCount = 0 };
}

// 3. Define actions
public record IncrementCounterAction();
public record DecrementCounterAction();

// 4. Define reducers
public static class CounterReducers
{
    [ReducerMethod]
    public static CounterState ReduceIncrementCounterAction(CounterState state, IncrementCounterAction action) =>
        state with { CurrentCount = state.CurrentCount + 1 };
        
    [ReducerMethod]
    public static CounterState ReduceDecrementCounterAction(CounterState state, DecrementCounterAction action) =>
        state with { CurrentCount = state.CurrentCount - 1 };
}
   // 5. Use in your components
@page "/counter"
@inherits FluxorComponent
@inject IState<CounterState> CounterState
@inject IDispatcher Dispatcher

<h1>Counter</h1>

<p>Current count: @CounterState.Value.CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Increment</button>
<button class="btn btn-secondary" @onclick="DecrementCount">Decrement</button>

@code {
    private void IncrementCount() => Dispatcher.Dispatch(new IncrementCounterAction());
    private void DecrementCount() => Dispatcher.Dispatch(new DecrementCounterAction());
}
   // 6. Setup in Program.cs
builder.Services.AddFluxor(options => options
    .ScanAssemblies(typeof(Program).Assembly)
    .UseReduxDevTools());

The key advantage of Fluxor is its integration with Redux DevTools, which allows you to inspect state changes and travel through time to understand how your application state evolves.

5. Local Storage State Persistence

To persist state across browser sessions, you can combine any of the above approaches with browser storage. The Blazored.LocalStorage library provides a convenient way to access the browser’s localStorage API:

   # Install packages
dotnet add package Blazored.LocalStorage
   // Register in Program.cs
builder.Services.AddBlazoredLocalStorage();
   // Use in a component or service
@inject ILocalStorageService LocalStorage

// Save state
await LocalStorage.SetItemAsync("theme", "dark");

// Load state
var theme = await LocalStorage.GetItemAsync<string>("theme");

Component Libraries

As of 2025, there are several mature component libraries for Blazor, each with its own strengths. We’ll focus on two of the most popular: MudBlazor and Radzen.

MudBlazor

MudBlazor is a Material Design-based component library that has become one of the most popular choices for Blazor developers due to its comprehensive set of components, ease of use, and minimal JavaScript dependencies.

Setting Up MudBlazor

   dotnet add package MudBlazor

In Program.cs:

   using MudBlazor.Services;

// ...
builder.Services.AddMudServices();

In App.razor:

   <MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />

MudBlazor Component Examples

Data Grid with Built-in Filtering and Sorting
   <MudCard>
    <MudCardHeader>
        <MudText Typo="Typo.h5">Employees</MudText>
    </MudCardHeader>
    <MudCardContent>
        <MudDataGrid T="Employee" Items="@employees" Filterable="true" SortMode="SortMode.Multiple" 
                     Hideable="true" Hover="true" Striped="true" Virtualize="true">
            <Columns>
                <PropertyColumn Property="e => e.Id" Title="ID" Sortable="true" />
                <PropertyColumn Property="e => e.FirstName" Title="First Name" Sortable="true" />
                <PropertyColumn Property="e => e.LastName" Title="Last Name" Sortable="true" />
                <PropertyColumn Property="e => e.Department" Title="Department" Sortable="true" />
                <PropertyColumn Property="e => e.Salary" Title="Salary" Sortable="true" Format="C" />
                <TemplateColumn Title="Actions">
                    <CellTemplate>
                        <MudButton Variant="Variant.Filled" Color="Color.Primary" 
                                  @onclick="() => ViewDetails(context.Item)">
                            View
                        </MudButton>
                        <MudButton Variant="Variant.Filled" Color="Color.Error" 
                                  @onclick="() => DeleteEmployee(context.Item)">
                            Delete
                        </MudButton>
                    </CellTemplate>
                </TemplateColumn>
            </Columns>
        </MudDataGrid>
    </MudCardContent>
</MudCard>

@code {
    private List<Employee> employees = new();
    
    protected override void OnInitialized()
    {
        // Populate employees list
        employees = GetEmployees();
    }
    
    private List<Employee> GetEmployees() => new()
    {
        new Employee { Id = 1, FirstName = "John", LastName = "Doe", Department = "Engineering", Salary = 90000 },
        new Employee { Id = 2, FirstName = "Jane", LastName = "Smith", Department = "Marketing", Salary = 85000 },
        // More employees...
    };
    
    private void ViewDetails(Employee employee)
    {
        // Handle view details
    }
    
    private void DeleteEmployee(Employee employee)
    {
        // Handle delete
    }
    
    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Department { get; set; }
        public decimal Salary { get; set; }
    }
}
Form with Validation
   <MudForm @ref="form" @bind-IsValid="@success" @bind-Errors="@errors">
    <MudCard>
        <MudCardContent>
            <MudTextField T="string" Label="First Name" Required="true" RequiredError="First name is required!"
                          @bind-Value="employee.FirstName" />
            <MudTextField T="string" Label="Last Name" Required="true" RequiredError="Last name is required!"
                          @bind-Value="employee.LastName" />
            <MudSelect T="string" Label="Department" Required="true" RequiredError="Department is required!"
                      @bind-Value="employee.Department">
                <MudSelectItem Value="@("Engineering")">Engineering</MudSelectItem>
                <MudSelectItem Value="@("Marketing")">Marketing</MudSelectItem>
                <MudSelectItem Value="@("Sales")">Sales</MudSelectItem>
                <MudSelectItem Value="@("HR")">HR</MudSelectItem>
            </MudSelect>
            <MudNumericField T="decimal" Label="Salary" Required="true" Min="30000" Max="200000" Format="N2"
                            @bind-Value="employee.Salary" />
        </MudCardContent>
        <MudCardActions>
            <MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!success)"
                      @onclick="SubmitForm">Submit</MudButton>
            <MudButton Variant="Variant.Outlined" Color="Color.Secondary"
                      @onclick="ResetForm">Reset</MudButton>
        </MudCardActions>
    </MudCard>
</MudForm>

@code {
    private MudForm form;
    private bool success;
    private string[] errors = { };
    private Employee employee = new();
    
    private void SubmitForm()
    {
        // Handle form submission
    }
    
    private void ResetForm()
    {
        employee = new();
        form.Reset();
    }
    
    public class Employee
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Department { get; set; }
        public decimal Salary { get; set; } = 50000;
    }
}

Radzen Blazor

Radzen Blazor is another popular component library offering over 90 UI controls for building rich web applications. It’s known for its comprehensive set of controls and excellent documentation.

Setting Up Radzen Blazor

   dotnet add package Radzen.Blazor

In Program.cs:

   // Add default Radzen services
builder.Services.AddScoped<DialogService>();
builder.Services.AddScoped<NotificationService>();
builder.Services.AddScoped<TooltipService>();
builder.Services.AddScoped<ContextMenuService>();

In _Layout.cshtml or App.razor:

   <link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>

Radzen Component Examples

Data Grid with CRUD Operations
   @page "/employees"
@using System.Linq.Dynamic.Core
@inject DialogService DialogService
@inject NotificationService NotificationService

<h1>Employees</h1>

<RadzenButton Icon="add_circle_outline" Text="Add New Employee" Click="@AddNewEmployee" 
             ButtonStyle="ButtonStyle.Success" class="mb-3" />

<RadzenDataGrid @ref="grid" AllowFiltering="true" AllowColumnResize="true" 
                AllowAlternatingRows="true" FilterMode="FilterMode.Advanced" AllowSorting="true" 
                PageSize="10" AllowPaging="true" PagerHorizontalAlign="HorizontalAlign.Left" 
                ShowPagingSummary="true" Data="@employees" TItem="Employee" 
                ColumnWidth="300px" LogicalFilterOperator="LogicalFilterOperator.Or">
    <Columns>
        <RadzenDataGridColumn TItem="Employee" Property="Id" Title="ID" Width="80px" TextAlign="TextAlign.Center" />
        <RadzenDataGridColumn TItem="Employee" Property="FirstName" Title="First Name" Width="150px" />
        <RadzenDataGridColumn TItem="Employee" Property="LastName" Title="Last Name" Width="150px" />
        <RadzenDataGridColumn TItem="Employee" Property="Department" Title="Department" Width="150px" />
        <RadzenDataGridColumn TItem="Employee" Property="Salary" Title="Salary" Width="150px" FormatString="{0:C}" />
        <RadzenDataGridColumn TItem="Employee" Title="Actions" Width="120px" TextAlign="TextAlign.Center">
            <Template Context="employee">
                <RadzenButton Icon="edit" ButtonStyle="ButtonStyle.Light" 
                             Click="@(args => EditEmployee(employee))" />
                <RadzenButton Icon="delete" ButtonStyle="ButtonStyle.Danger" 
                             Click="@(args => DeleteEmployee(employee))" />
            </Template>
        </RadzenDataGridColumn>
    </Columns>
</RadzenDataGrid>

@code {
    RadzenDataGrid<Employee> grid;
    IEnumerable<Employee> employees;
    
    protected override void OnInitialized()
    {
        employees = GetEmployees();
    }
    
    private List<Employee> GetEmployees() => new()
    {
        new Employee { Id = 1, FirstName = "John", LastName = "Doe", Department = "Engineering", Salary = 90000 },
        new Employee { Id = 2, FirstName = "Jane", LastName = "Smith", Department = "Marketing", Salary = 85000 },
        // More employees...
    };
    
    async Task AddNewEmployee()
    {
        var result = await DialogService.OpenAsync<EmployeeEditor>("Add Employee", 
            new Dictionary<string, object>() { { "Employee", new Employee() } });
        
        if (result != null)
        {
            var newEmployee = (Employee)result;
            var employeeList = employees.ToList();
            
            newEmployee.Id = employeeList.Count > 0 ? employeeList.Max(e => e.Id) + 1 : 1;
            employeeList.Add(newEmployee);
            employees = employeeList;
            
            NotificationService.Notify(NotificationSeverity.Success, "Success", "Employee added successfully");
            await grid.Reload();
        }
    }
    
    async Task EditEmployee(Employee employee)
    {
        var result = await DialogService.OpenAsync<EmployeeEditor>("Edit Employee", 
            new Dictionary<string, object>() { { "Employee", new Employee 
            { 
                Id = employee.Id, 
                FirstName = employee.FirstName, 
                LastName = employee.LastName, 
                Department = employee.Department, 
                Salary = employee.Salary 
            } } });
        
        if (result != null)
        {
            var updatedEmployee = (Employee)result;
            var employeeList = employees.ToList();
            var index = employeeList.FindIndex(e => e.Id == employee.Id);
            
            if (index != -1)
            {
                employeeList[index] = updatedEmployee;
                employees = employeeList;
                
                NotificationService.Notify(NotificationSeverity.Success, "Success", "Employee updated successfully");
                await grid.Reload();
            }
        }
    }
    
    async Task DeleteEmployee(Employee employee)
    {
        var confirm = await DialogService.Confirm("Are you sure you want to delete this employee?", 
            "Delete Employee", new ConfirmOptions() { OkButtonText = "Yes", CancelButtonText = "No" });
        
        if (confirm.HasValue && confirm.Value)
        {
            var employeeList = employees.ToList();
            var index = employeeList.FindIndex(e => e.Id == employee.Id);
            
            if (index != -1)
            {
                employeeList.RemoveAt(index);
                employees = employeeList;
                
                NotificationService.Notify(NotificationSeverity.Success, "Success", "Employee deleted successfully");
                await grid.Reload();
            }
        }
    }
    
    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Department { get; set; }
        public decimal Salary { get; set; }
    }
}
Employee Editor Dialog Component
   @inject DialogService DialogService

<RadzenTemplateForm TItem="Employee" Data="@Employee" Submit="OnSubmit">
    <div class="row">
        <div class="col-md-6 mb-3">
            <RadzenFormField Text="First Name" Variant="Variant.Outlined">
                <RadzenTextBox @bind-Value="@Employee.FirstName" Name="FirstName" />
                <RadzenRequiredValidator Component="FirstName" Text="First name is required" />
            </RadzenFormField>
        </div>
        <div class="col-md-6 mb-3">
            <RadzenFormField Text="Last Name" Variant="Variant.Outlined">
                <RadzenTextBox @bind-Value="@Employee.LastName" Name="LastName" />
                <RadzenRequiredValidator Component="LastName" Text="Last name is required" />
            </RadzenFormField>
        </div>
    </div>
    <div class="row">
        <div class="col-md-6 mb-3">
            <RadzenFormField Text="Department" Variant="Variant.Outlined">
                <RadzenDropDown @bind-Value="@Employee.Department" Data="@departments" 
                               TextProperty="Name" ValueProperty="Id" Name="Department" />
                <RadzenRequiredValidator Component="Department" Text="Department is required" />
            </RadzenFormField>
        </div>
        <div class="col-md-6 mb-3">
            <RadzenFormField Text="Salary" Variant="Variant.Outlined">
                <RadzenNumeric @bind-Value="@Employee.Salary" Name="Salary" />
                <RadzenRequiredValidator Component="Salary" Text="Salary is required" />
                <RadzenNumericRangeValidator Component="Salary" Min="30000" Max="200000" 
                                          Text="Salary must be between $30,000 and $200,000" />
            </RadzenFormField>
        </div>
    </div>
    <div class="row">
        <div class="col d-flex justify-content-end">
            <RadzenButton ButtonType="ButtonType.Submit" Text="Save" ButtonStyle="ButtonStyle.Primary" />
            <RadzenButton Text="Cancel" ButtonStyle="ButtonStyle.Light" Click="@Cancel" Class="ml-2" />
        </div>
    </div>
</RadzenTemplateForm>

@code {
    [Parameter]
    public Employee Employee { get; set; }
    
    private List<Department> departments = new()
    {
        new Department { Id = "Engineering", Name = "Engineering" },
        new Department { Id = "Marketing", Name = "Marketing" },
        new Department { Id = "Sales", Name = "Sales" },
        new Department { Id = "HR", Name = "HR" }
    };
    
    private void OnSubmit()
    {
        DialogService.Close(Employee);
    }
    
    private void Cancel()
    {
        DialogService.Close();
    }
    
    public class Department
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
}

MudBlazor vs. Radzen: Which to Choose?

Here’s a comparison table to help you decide between MudBlazor and Radzen in 2025:

FeatureMudBlazorRadzen Blazor
Design SystemMaterial DesignMultiple themes (Material, Fluent UI, etc.)
Component Count70+90+
JavaScript DependenciesMinimalModerate
ThemingExcellent, built-in theme designerGood, predefined themes
PerformanceVery goodGood
Learning CurveLow to moderateModerate
DocumentationExcellentExcellent
Community SupportVery activeActive
LicenseMITFree for commercial use

When to choose MudBlazor:

  • You prefer Material Design aesthetics
  • You want minimal JavaScript dependencies
  • You need extensive theming capabilities
  • Your project benefits from MudBlazor’s more compact bundle size

When to choose Radzen:

  • You need the widest range of components
  • You want multiple theme options
  • You prefer Radzen’s specific components (like the powerful DataGrid)
  • You’re already using Radzen tools in your development workflow

Performance Optimization Techniques

Blazor applications can face performance challenges, especially in WebAssembly mode. Here are the most effective techniques for optimizing Blazor performance in 2025:

1. Ahead-of-Time (AOT) Compilation

AOT compilation significantly improves WebAssembly performance by pre-compiling your .NET code to WebAssembly, rather than interpreting it at runtime:

   <!-- In your .csproj file -->
<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
  <!-- Optional: Trim IL after AOT compilation for even smaller bundles -->
  <WasmStripILAfterAOT>true</WasmStripILAfterAOT>
</PropertyGroup>

2. Code Splitting and Lazy Loading

Reduce the initial load time by lazy loading assemblies that aren’t immediately needed:

   <!-- In your .csproj file -->
<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="ChartComponents.dll" />
  <BlazorWebAssemblyLazyLoad Include="AdvancedFeatures.dll" />
</ItemGroup>

Then, use the LazyAssemblyLoader service to load the assemblies when needed:

   @page "/charts"
@using System.Reflection
@inject LazyAssemblyLoader LazyLoader

@if (chartsLoaded)
{
    <DynamicComponent Type="@chartComponentType" Parameters="@parameters" />
}
else
{
    <MudProgressCircular Indeterminate="true" />
}

@code {
    private bool chartsLoaded;
    private Type chartComponentType;
    private Dictionary<string, object> parameters = new();
    
    protected override async Task OnInitializedAsync()
    {
        var assemblies = await LazyLoader.LoadAssembliesAsync(new[] { "ChartComponents.dll" });
        var assembly = assemblies.FirstOrDefault();
        
        if (assembly != null)
        {
            chartComponentType = assembly.GetType("ChartComponents.BarChart");
            parameters["Data"] = GetChartData();
            chartsLoaded = true;
        }
    }
    
    private List<DataPoint> GetChartData() => new()
    {
        new DataPoint { Label = "Jan", Value = 42 },
        new DataPoint { Label = "Feb", Value = 57 },
        // More data...
    };
    
    public class DataPoint
    {
        public string Label { get; set; }
        public double Value { get; set; }
    }
}

3. Virtualization for Long Lists

Use Blazor’s built-in virtualization to efficiently render long lists by only rendering the items currently in view:

   <div style="height: 500px; overflow-y: auto;">
    <Virtualize Items="@largeDataSet" Context="item" OverscanCount="10">
        <div class="data-item">
            <h3>@item.Title</h3>
            <p>@item.Description</p>
        </div>
    </Virtualize>
</div>

@code {
    private List<DataItem> largeDataSet = Enumerable.Range(1, 10000)
        .Select(i => new DataItem 
        { 
            Id = i, 
            Title = $"Item {i}", 
            Description = $"Description for item {i}" 
        })
        .ToList();
    
    public class DataItem
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
    }
}

For even more control, specify the item size:

   <Virtualize Items="@largeDataSet" Context="item" ItemSize="50">
    <div style="height: 50px;">@item.Title</div>
</Virtualize>

4. Optimize Rendering

Implement ShouldRender() to avoid unnecessary renders:

   @inherits ComponentBase

@code {
    private string previousValue;
    
    [Parameter]
    public string Value { get; set; }
    
    protected override bool ShouldRender()
    {
        if (Value != previousValue)
        {
            previousValue = Value;
            return true;
        }
        
        return false;
    }
}

5. Memory Management

Proper memory management is crucial, especially in Blazor Server apps:

   @implements IDisposable

@code {
    private System.Threading.Timer timer;
    
    protected override void OnInitialized()
    {
        timer = new System.Threading.Timer(
            _ => InvokeAsync(() => {
                // Update UI
                StateHasChanged();
            }),
            null,
            TimeSpan.Zero,
            TimeSpan.FromSeconds(1)
        );
    }
    
    public void Dispose()
    {
        timer?.Dispose();
    }
}

6. Bundle Size Optimization

Reduce the size of your Blazor WebAssembly bundle:

Enable IL trimming:

   <PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <TrimMode>link</TrimMode>
</PropertyGroup>

Use compression:

   // In Program.cs of your ASP.NET Core host
app.UseResponseCompression();

// Register compression services
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = System.IO.Compression.CompressionLevel.Optimal;
});

7. Use CSS Isolation

Blazor’s CSS isolation helps keep styles scoped to specific components, improving both maintainability and performance:

   /* In Counter.razor.css */
.counter-container {
    border: 1px solid #ccc;
    padding: 20px;
    border-radius: 5px;
}

.counter-value {
    font-size: 2rem;
    font-weight: bold;
    color: #007bff;
}

.counter-button {
    margin-top: 10px;
}
   <!-- In Counter.razor -->
<div class="counter-container">
    <h2>Counter</h2>
    <p class="counter-value">@currentCount</p>
    <button class="counter-button" @onclick="IncrementCount">Increment</button>
</div>

@code {
    private int currentCount = 0;
    
    private void IncrementCount()
    {
        currentCount++;
    }
}

Real-World Application Example

Let’s tie everything together with a real-world application example that demonstrates state management, component libraries, and performance optimization techniques. This example is for a task management application:

Project Structure (Conceptual)

   TaskManager/
├── Models/
│   ├── TaskItem.cs
│   └── Project.cs
├── Services/
│   ├── TaskState.cs
│   ├── ProjectService.cs
│   └── AuthService.cs
├── Features/
│   ├── Counter/
│   │   ├── CounterState.cs
│   │   ├── Actions.cs
│   │   └── Reducers.cs
│   └── Tasks/
│       ├── TasksState.cs
│       ├── Actions.cs
│       └── Reducers.cs
├── Components/
│   ├── TaskList.razor
│   ├── TaskItem.razor
│   ├── TaskForm.razor
│   └── ProjectSelector.razor
├── Pages/
│   ├── Dashboard.razor
│   ├── Projects.razor
│   ├── TaskDetails.razor
│   └── Settings.razor
└── Shared/
    ├── MainLayout.razor
    └── NavMenu.razor

Task State Management with Fluxor

   // TasksState.cs
public record TasksState
{
    public bool IsLoading { get; init; }
    public List<TaskItem> Tasks { get; init; } = new();
    public string ErrorMessage { get; init; }
}

// Actions.cs
public record FetchTasksAction();
public record FetchTasksSuccessAction(List<TaskItem> Tasks);
public record FetchTasksFailureAction(string ErrorMessage);
public record AddTaskAction(TaskItem Task);
public record UpdateTaskAction(TaskItem Task);
public record DeleteTaskAction(int TaskId);

// Reducers.cs
public static class TasksReducers
{
    [ReducerMethod]
    public static TasksState ReduceFetchTasksAction(TasksState state, FetchTasksAction action) =>
        state with { IsLoading = true, ErrorMessage = null };
        
    [ReducerMethod]
    public static TasksState ReduceFetchTasksSuccessAction(TasksState state, FetchTasksSuccessAction action) =>
        state with { IsLoading = false, Tasks = action.Tasks.ToList(), ErrorMessage = null };
        
    [ReducerMethod]
    public static TasksState ReduceFetchTasksFailureAction(TasksState state, FetchTasksFailureAction action) =>
        state with { IsLoading = false, ErrorMessage = action.ErrorMessage };
        
    [ReducerMethod]
    public static TasksState ReduceAddTaskAction(TasksState state, AddTaskAction action)
    {
        var tasks = state.Tasks.ToList();
        tasks.Add(action.Task);
        return state with { Tasks = tasks };
    }
    
    [ReducerMethod]
    public static TasksState ReduceUpdateTaskAction(TasksState state, UpdateTaskAction action)
    {
        var tasks = state.Tasks.ToList();
        var index = tasks.FindIndex(t => t.Id == action.Task.Id);
        
        if (index >= 0)
        {
            tasks[index] = action.Task;
        }
        
        return state with { Tasks = tasks };
    }
    
    [ReducerMethod]
    public static TasksState ReduceDeleteTaskAction(TasksState state, DeleteTaskAction action)
    {
        var tasks = state.Tasks.Where(t => t.Id != action.TaskId).ToList();
        return state with { Tasks = tasks };
    }
}

Task List Component with MudBlazor and Virtualization

   @page "/tasks"
@inherits FluxorComponent
@inject IState<TasksState> TasksState
@inject IDispatcher Dispatcher

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudText Typo="Typo.h3" Class="mb-4">Tasks</MudText>
    
    <MudPaper Class="pa-4 mb-4">
        <TaskForm OnSubmit="AddTask" />
    </MudPaper>
    
    @if (TasksState.Value.IsLoading)
    {
        <MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
    }
    else if (!string.IsNullOrEmpty(TasksState.Value.ErrorMessage))
    {
        <MudAlert Severity="Severity.Error" Class="mb-4">@TasksState.Value.ErrorMessage</MudAlert>
    }
    else
    {
        <MudPaper Class="pa-0" Style="height: 500px; overflow-y: auto;">
            <Virtualize Items="@TasksState.Value.Tasks" Context="task" OverscanCount="5">
                <TaskItem Task="@task" 
                         OnStatusChanged="UpdateTaskStatus"
                         OnDelete="DeleteTask" />
            </Virtualize>
        </MudPaper>
    }
</MudContainer>

@code {
    // Assume TaskItem model exists
    // Assume TaskForm component exists

    protected override void OnInitialized()
    {
        Dispatcher.Dispatch(new FetchTasksAction()); // Assuming effects handle the API call
        base.OnInitialized();
    }
    
    private void AddTask(TaskItem task)
    {
        Dispatcher.Dispatch(new AddTaskAction(task)); // Assuming effects handle the API call
    }
    
    private void UpdateTaskStatus(TaskItem task)
    {
        Dispatcher.Dispatch(new UpdateTaskAction(task)); // Assuming effects handle the API call
    }
    
    private void DeleteTask(int taskId)
    {
        Dispatcher.Dispatch(new DeleteTaskAction(taskId)); // Assuming effects handle the API call
    }
}

Task Item Component with CSS Isolation

   <!-- TaskItem.razor -->
<MudPaper Elevation="2" Class="pa-4 ma-2 task-item @GetStatusClass()">
    <MudGrid>
        <MudItem xs="1">
            <MudCheckBox Checked="@Task.IsCompleted" 
                         CheckedChanged="@(value => OnStatusChange(value))" />
        </MudItem>
        <MudItem xs="8">
            <MudText Typo="Typo.h6" Class="@(Task.IsCompleted ? "completed-task" : "")">
                @Task.Title
            </MudText>
            <MudText Typo="Typo.body2">
                @Task.Description
            </MudText>
            <MudChip Color="Color.Primary" Size="Size.Small" Class="mt-2">
                @Task.Project
            </MudChip>
            <MudChip Color="GetPriorityColor()" Size="Size.Small" Class="mt-2 ml-2">
                @Task.Priority
            </MudChip>
        </MudItem>
        <MudItem xs="2">
            <MudText Typo="Typo.body2">
                Due: @Task.DueDate.ToShortDateString()
            </MudText>
        </MudItem>
        <MudItem xs="1">
            <MudIconButton Icon="@Icons.Material.Filled.Delete" 
                          Color="Color.Error"
                          OnClick="@(() => OnDeleteClicked())" />
        </MudItem>
    </MudGrid>
</MudPaper>

@code {
    [Parameter]
    public TaskItem Task { get; set; } // Assume TaskItem model exists
    
    [Parameter]
    public EventCallback<TaskItem> OnStatusChanged { get; set; }
    
    [Parameter]
    public EventCallback<int> OnDelete { get; set; }
    
    private async Task OnStatusChange(bool value)
    {
        Task.IsCompleted = value;
        await OnStatusChanged.InvokeAsync(Task);
    }
    
    private async Task OnDeleteClicked()
    {
        await OnDelete.InvokeAsync(Task.Id);
    }
    
    private string GetStatusClass() => Task.IsCompleted ? "completed" : "";
    
    private Color GetPriorityColor() => Task.Priority switch
    {
        "High" => Color.Error,
        "Medium" => Color.Warning,
        "Low" => Color.Success,
        _ => Color.Default
    };
}
   /* TaskItem.razor.css */
.task-item {
    transition: background-color 0.3s ease;
}

.task-item:hover {
    background-color: #f5f5f5;
}

.task-item.completed {
    background-color: #f0f8f0;
}

.completed-task {
    text-decoration: line-through;
    color: #888;
}

Task Effects for API Calls

   public class TasksEffects
{
    private readonly ITaskService _taskService; // Assume ITaskService exists
    
    public TasksEffects(ITaskService taskService)
    {
        _taskService = taskService;
    }
    
    [EffectMethod]
    public async Task HandleFetchTasksAction(FetchTasksAction action, IDispatcher dispatcher)
    {
        try
        {
            var tasks = await _taskService.GetTasksAsync();
            dispatcher.Dispatch(new FetchTasksSuccessAction(tasks));
        }
        catch (Exception ex)
        {
            dispatcher.Dispatch(new FetchTasksFailureAction(ex.Message));
        }
    }
    
    [EffectMethod]
    public async Task HandleAddTaskAction(AddTaskAction action, IDispatcher dispatcher)
    {
        try
        {
            await _taskService.AddTaskAsync(action.Task);
            // Optionally dispatch a success action or refetch tasks
        }
        catch (Exception ex)
        {
            // Error handling, maybe dispatch a failure action
            Console.WriteLine($"Error adding task: {ex.Message}");
        }
    }
    
    // Similar effect methods for UpdateTaskAction and DeleteTaskAction
}

Optimizing with Lazy Loading

   // In Program.cs
builder.Services.AddScoped<LazyAssemblyLoader>();
   // In .csproj
<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="TaskReporting.dll" />
</ItemGroup>
   // In ReportsPage.razor
@page "/reports"
@inject LazyAssemblyLoader LazyLoader

@if (reportsComponentLoaded)
{
    <DynamicComponent Type="@reportsComponentType" />
}
else
{
    <MudProgressCircular Indeterminate="true" />
}

@code {
    private bool reportsComponentLoaded;
    private Type reportsComponentType;
    
    protected override async Task OnInitializedAsync()
    {
        var assemblies = await LazyLoader.LoadAssembliesAsync(new[] { "TaskReporting.dll" });
        var assembly = assemblies.FirstOrDefault();
        
        if (assembly != null)
        {
            reportsComponentType = assembly.GetType("TaskManager.Reporting.ReportsDashboard"); // Adjust namespace/type
            reportsComponentLoaded = true;
        }
    }
}

Conclusion and Future Outlook

As of 2025, Blazor has matured into a powerful framework for building modern web UIs, with robust solutions for state management, comprehensive component libraries, and effective performance optimization techniques.

Key Takeaways

  • State Management: Choose the right approach based on your application’s complexity: component state for simple scenarios, service-based state for moderate complexity, and Flux patterns (e.g., Fluxor) for complex, enterprise applications.
  • Component Libraries: Both MudBlazor and Radzen offer excellent component sets. MudBlazor provides a cohesive Material Design experience with minimal JavaScript, while Radzen offers more components and flexibility with multiple theming options.
  • Performance Optimization: Apply techniques like AOT compilation (for WASM), lazy loading, virtualization, ShouldRender() optimization, proper memory management, bundle size reduction, and CSS isolation to ensure responsive Blazor applications.

Future Outlook

Looking ahead, we can expect Blazor to continue evolving with:

  • Further performance improvements in the WebAssembly runtime.
  • Enhanced tooling for debugging and profiling.
  • Better integration with other Microsoft technologies.
  • More sophisticated component libraries.
  • Expanded ecosystem of third-party packages and tools.

By applying the techniques and practices outlined in this article, you can build modern, performant, and maintainable web UIs with Blazor in 2025 and beyond. Mastering state management, leveraging component libraries, and implementing performance optimizations are key to creating feature-rich web applications using C# and .NET.