I'm working on a simple project to learn Blazor Server and EF Core, partly generated by an AI assistant. I've noticed that a specific test page, while not complex, has a very long initial load time (around 8 seconds). This kind of delay would be unacceptable in a real-world commercial project.
To diagnose this, I implemented a timing mechanism. Here's what I've found:
Without
DbContextwarm-up, the first page load takes ~8.1 secondsWith
DbContextwarm-up, my application startup log shows theDbContexttakes ~7.9 seconds to warm up. However, the first page load still takes ~7.8 seconds
This doesn't seem right. It appears that warming up the DbContext has almost no effect on the user-perceived initial load time. My understanding was that EF Core's model compilation was a major part of the cold start, but my results suggest another bottleneck of similar magnitude exists.
My question
Is this long initial load time (~8 seconds) normal for a Blazor Server application's cold start, even after the DbContext has been pre-warmed? If not, what could be the bottleneck? Is it the Blazor framework itself, SignalR connection setup, or something else I'm missing?
Here is the relevant code for my project:
Program.cs (with warm-up logic):
// using statements...var builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddRazorComponents() .AddInteractiveServerComponents();var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");builder.Services.AddDbContextFactory<AppDbContext>(options => options.UseNpgsql(connectionString));builder.Services.AddSingleton<PageLoadTimerService>();var app = builder.Build();// --- Application Warm-up ---Console.WriteLine("Application is starting, beginning warm-up...");using (var scope = app.Services.CreateScope()){ var timer = new Stopwatch(); timer.Start(); var services = scope.ServiceProvider; try { var dbContextFactory = services.GetRequiredService<IDbContextFactory<AppDbContext>>(); await using (var dbContext = await dbContextFactory.CreateDbContextAsync()) { await dbContext.Database.CanConnectAsync(); timer.Stop(); Console.WriteLine($"Database context has been successfully warmed up, took {timer.ElapsedMilliseconds} ms."); } } catch (Exception ex) { // ... error logging ... }}// ... rest of Program.cs ...app.Run();DbTest.razor (the slow-loading page):
@page "/db-test" @using System.Diagnostics @using Microsoft.EntityFrameworkCore @using BlazorApp.Data @using BlazorApp.Model @using BlazorApp.Services @inject IDbContextFactory<AppDbContext> DbFactory @inject PageLoadTimerService GlobalTimerService @rendermode InteractiveServer<PageTitle>Database Relation Test</PageTitle> @if (fullLoadTimeInMs.HasValue) {<div class="alert alert-info"><strong>Full Load Time (from click to completion):</strong><span>@fullLoadTimeInMs.Value.ToString("F2") ms</span></div> } @if(hotLoadTimeInMs.HasValue) {<div class="alert alert-secondary"><strong>OnInitializedAsync Duration (Hot Load):</strong><span>@hotLoadTimeInMs.Value.ToString("F2") ms</span></div> }<h1>Database Relation Test</h1><div class="row"><!-- Action Panel --><div class="col-md-4"><h3>Action Panel</h3><div class="d-grid gap-2"><button class="btn btn-primary" @onclick="CreatePostWithTags">1. Create New Post with Tags</button><button class="btn btn-info" @onclick="LoadPosts">2. Load/Refresh Posts</button><button class="btn btn-success" @onclick="UpdateFirstPostTags" disabled="@(posts.Count == 0)">3. Update First Post's Tags</button><button class="btn btn-danger" @onclick="DeleteFirstPost" disabled="@(posts.Count == 0)">4. Delete First Post</button><button class="btn btn-secondary" @onclick="ClearLogs">Clear Logs</button></div><hr /><h4>Status Log:</h4><div class="log-box bg-light p-2 rounded" style="height: 200px; overflow-y: scroll;"> @foreach (var log in logs) {<div class="@log.CssClass">@log.Message</div> }</div></div><!-- Data Display --><div class="col-md-8"><h3>Current Post List:</h3> @if (isLoading) {<p><em>Loading...</em></p> } else if (posts.Count == 0) {<p><em>No posts in the database. Please create one first.</em></p> } else {<ul class="list-group"> @foreach (var post in posts) {<li class="list-group-item"><h5>@post.Title <small class="text-muted">(ID: @post.Id)</small></h5><p>@post.Content</p><div> @foreach (var tag in post.Tags) {<span class="badge bg-secondary me-1">@tag.Name</span> }</div><small class="text-muted">Created at: @post.CreatedDate.ToLocalTime()</small></li> }</ul> }</div></div> @code { private double? fullLoadTimeInMs; private double? hotLoadTimeInMs; // ... other properties and methods ... protected override async Task OnInitializedAsync() { var hotLoadStopwatch = Stopwatch.StartNew(); await LoadPosts(); hotLoadStopwatch.Stop(); hotLoadTimeInMs = hotLoadStopwatch.Elapsed.TotalMilliseconds; fullLoadTimeInMs = GlobalTimerService.GetLastMeasurement(); StateHasChanged(); } private async Task LoadPosts() { // ... loads posts from DbContext ... } }PageLoadTimerService.cs (used for timing):
public class PageLoadTimerService{ private long _startTimeTicks = 0; private double? _lastMeasurement = null; public void Start() { _startTimeTicks = Stopwatch.GetTimestamp(); _lastMeasurement = null; } public double? GetLastMeasurement() { if (_startTimeTicks != 0) { var endTimeTicks = Stopwatch.GetTimestamp(); var elapsed = Stopwatch.GetElapsedTime(_startTimeTicks, endTimeTicks); _lastMeasurement = elapsed.TotalMilliseconds; _startTimeTicks = 0; } return _lastMeasurement; }}Any insights into why the initial load remains so slow would be greatly appreciated.