Techniques to Boost the Performance of Razor Pages

Aubrey Drescher
5 min readJan 5, 2024

--

Three ways to make an ASP.NET website behave more like a snappy modern mobile app.

Photo by Claudio Schwarz on Unsplash

Example 1: Speeding up page load time

I have an ASP.NET webpage that was taking so long to query the database and populate the data grid that my users started complaining. My first solution was to insert some JavaScript into the page so it wouldn’t refresh with a post-back every time they edited a record. But this didn’t really get to the heart of the problem, and the page remained painfully slow on the initial load.

Problem Diagnosis with Code Execution Timer

I decided to insert timers in order to find the source of the bottleneck.

using System.Diagnostics;
Stopwatch watch = Stopwatch.StartNew();

<code that you want to time>

watch.Stop();
long elapsedMs = watch.ElapsedMilliseconds;
Debug.WriteLine("Executed code in " + elapsedMs.ToString() + " ms");

I could then look in my Debug Output window in Visual Studio to see how long different parts of my code took to run.

First Solution: Efficient Querying

The culprit turned out to be an entity framework LINQ query that looked like this:

IQueryable<QCqueue> QCQueueQuery = from s in _context.QCqueue select s;
List<QCqueue> QCprojects = await QCQueueQuery.AsNoTracking().ToListAsync();

foreach (QCqueue QCproject in QCprojects) {
IQueryable<Closeoutqueue> CloseoutprojectQuery =
from c in _context.Closeoutqueue
where c.ProjectNumber.ToUpper().Equals(QCproject.ProjectNumber.ToUpper())
select c;

Closeoutqueue Closeoutproject = await CloseoutprojectQuery
.Include(x => x.Intersections).AsNoTracking().FirstOrDefaultAsync()
QCproject.IntersectionList = Closeoutproject.Intersections.ToList();
}

After reading about query optimization, I made two changes that sped things up. First, I replaced the foreach loop with a join. This way, I retrieved the list of intersection drawing numbers for each project in a single large query instead of many small queries.

The second change was less obvious and took more digging to discover. I have a nvarchar(max) field in my QCqueue database table, where people can store comments about each project. They can paste screenshots into the comments and those images get stored as long strings of encoded text.

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAvQAAAHiCAYAAACHu1gNAAAgAElEQVR4Ady9CfQfx1Xn++fxmAfDzJnz4B22B0xgMuwww/
YIy4PMMCTsEwiQEJKQEODlkATsrI5x7OA4m7M6ToKdkARnsSRLsi1b+77+te/7ZkuWJduyJWvfpfvOp3/6/nVVqu6u7l/1T3/P75zfqerq6qpbt27drW5XDw299oDxP3r0aKP/
f7/7vP3AbRdLn/nYzNM2dIPZjI0nijrPPvus7dq1y9asWWNLly4d
…this continues for a very long time…">

If I retrieve the full text of the comment for every project, it slows down the query. So I switched to retrieving the length of the comment instead. My new and improved query looks like this:

IQueryable<QCqueue> QCQueueQuery = 
from s in _context.QCqueue
join c in _context.Closeoutqueue
on s.ProjectNumber.ToUpper() equals c.ProjectNumber.ToUpper() into sc
from subc in sc.DefaultIfEmpty()
select new QCqueue {
Id = s.Id,
ProjectNumber = s.ProjectNumber,
IntersectionList = subc.Intersections.ToList(),
//Using Length as a flag to tell if comments exist
//Comment text is retrieved seperatley in the view component
QCComments = s.QCComments.Length.ToString(),
};

Second Solution: Razor View Component

If the comment length is greater than zero, I show a button that the user can click on to see the comment, and the full text with the images is retrieved at the time that the popup opens.

The popup is a Razor View Component. I call it in the page like so:

@await Component.InvokeAsync("QCComments", QCproject)

The code-behind runs this query on-demand:

public async Task<IViewComponentResult> InvokeAsync(QCqueue QCproject) {
string QCComments = await (from s in _context.QCqueue
where s.Id.Equals(QCproject.Id)
select s.QCComments).AsNoTracking().FirstOrDefaultAsync()

QCproject.QCComments = QCComments;
return View("QCComments", QCproject);
}

With these two improvements, the web page that used to load in ~45 seconds now loads in 5 seconds!

Example 2: Adding status notifications

One of the reasons my coworkers were refreshing this website frequently is so they could check if any new projects had been submitted for them to work on. One of them told me it would be great if I could add Facebook-style notification badges to the buttons on the sidebar. They would update automatically with the count of unassigned projects.

After I found some CSS to style these, I thought about how to build the infrastructure.

The HTML code for the sidebar was duplicated on every page, so the first thing I did was modularize it into a view component the same way I described above. When the view first loads, a query runs and populates the count variables in the ViewData object.

int UnassignedCloseoutCount = await countHelper.GetUnassignedCloseoutCount();
ViewData["UnassignedCloseoutCount"] = UnassignedCloseoutCount.ToString();
<span id="UnassignedCloseoutCount" class="notification" role="status">
@ViewData["UnassignedCloseoutCount"]</span>

Third Solution: AJAX on a repeating interval

How do I keep this number updated without a page refresh? Using AJAX!

Anytime, you can call any method that you have written anywhere in your codebase by referencing its location properly as the target URL of an HTTP request. I wrote this task in my Index.cshtml.cs file:

public async Task<JsonResult> OnGetUpdateSidebar()
{
try
{
CountHelper countHelper = new CountHelper(_context);
string resultText = await countHelper.GetAllUnassignedCounts();
return new JsonResult(resultText);
}
catch (Exception e)
{
return new JsonResult("ERROR: " + e.Message);
}
}

The task returns the result as JSON so it can be easily interpreted with JavaScript. I call the task with JQuery like this:

function updateNotification() { 
$.ajax({
type: "GET",
url: 'https://' + window.location.host +
'/<name of application>/?handler=UpdateSidebar'
}).done(function (result) {
//update your HTML with the results here
}
});
}

The magic is done with the “handler” parameter in the URL. Razor Pages have Named Handler Methods that can be used in a similar way to actions in MVC controllers.

Finally, I put my JQuery function on a repeating interval so it will run every 5 minutes (300K milliseconds) after the page first loads.

$(document).ready(function () {
setInterval(function () {
updateNotification();
}, 300000);
});

It isn’t a real-time update like a Facebook push notification would be, but it meets the same need!

--

--