Using ASP.NET Core to optimize page speed
Page speed is one of those underrated but critical factors that can directly impact the bottom line of a company, as it affects both customer experience and search engine ranking.
In my experience, it usually only takes a few hundred milliseconds or less for the server to generate the HTML document, while the majority of the time is spent on requesting and downloading additional resources such as CSS, JavaScript, and images. You can easily test this on your own website by using the developer tools that come with Chrome, Firefox, or Safari.
Considering how relatively little time is spent on generating the HTML document, it makes sense to focus our optimization efforts on other parts of our web application. Interestingly, it is relatively easy to implement a series of tactics that can help us accomplish our objective of optimizing page speed.
File compression
The first tactic is to compress our files before sending them over the network. By compressing files such as CSS, JavaScript, and images, we improve the time it takes to download the content. Let us consider the following example where we compare Gzip and Brotli to the original file.
Based on the table above, let us imagine we are running a website with 100M MAU (monthly active users) who, on average, visit 2 pages. If every page downloads the 3 original files, this equals 66.3 terabytes (100M x 2 x 331.41kB), compared to 15.7 terabytes (100M x 2 x 78.3kB) when using Gzip. We are not only improving the time it takes to download the data but also significantly reducing network traffic.
How to implement file compression in ASP.NET Core
Adding file compression is relatively easy and can be done using the middleware.
Add the following to our “program.cs”:
builder.Services.AddResponseCompression(options =>
{
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat([
/* Defaults
"application/javascript",
"application/json",
"application/xml",
"text/css",
"text/html",
"text/json",
"text/plain",
"text/xml",
*/
"text/javascript",
"image/jpeg",
"image/gif",
"image/png",
"image/svg+xml"
]);
options.ExcludedMimeTypes = [];
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
app.UseResponseCompression();
The type of compression that will be applied depends on what is supported by the browser, which is indicated in the “Accept-Encoding” header in the HTTP request. In the example above, if the browser does not support Brotli (for whatever reason), it will fall back to Gzip. While there are performance differences between these two algorithms, both of them perform quite well.
Finally, it is also important to note that real-time compression might lead to a potential security vulnerability if you have EnableForHttps set to true. According to Microsoft, dynamically generated content can be exposed to CRIME and BREACH attacks.
Minify code
Minifying CSS and JavaScript is another tactic we can use to improve the performance of our website. The goal of minification is to reduce the file size, but instead of using a compression algorithm, it removes unnecessary characters (like white spaces, comments, and line breaks) from your code without affecting its functionality.
Just as with file compression, we see that there are significant reductions in file size when minifying our code. And when we combine both minification and Gzip, we get an even greater reduction in file size
How to implement code minification
In order to minify code in ASP.NET Core we need to install a tool via NuGet. In this example we are using the WebOptimizer.
Add the following to our “program.cs” :
builder.Services.AddWebOptimizer(pipeline => {
pipeline.MinifyJsFiles(
"/lib/jquery/dist/jquery.js",
"/lib/bootstrap/dist/js/bootstrap.js"
);
pipeline.MinifyCssFiles(
"/lib/bootstrap/dist/css/bootstrap.css"
);
});
app.UseWebOptimizer(); // add before app.UseStaticFiles();
Add the following to our HTML file:
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
In case the files do not get minified, it might be due to either the CSS or JS not being well-formed.
Reduce HTTP requests
A significant impact on page speed is the number of components (e.g., CSS, JS, IMG) included in a webpage. If we open up our favorite browser developer tool, we can see that each component generates a request. Inspecting a given request will most likely also show that the waiting time (TTFB) is significantly longer than the download of the component.
A reason behind the long waiting time is caused by browsers that limit the number of concurrent connections. If the total number of components are larger than the available connections, the rest of the components will be added to a queue and blocked from being downloaded. Furthermore, when a browser encounters a JavaScript component, it will block all other components from being downloaded simultaneously. The reason for this is to ensure that scripts are downloaded and executed in the correct order.
If we are running a webpage with many components, then the waiting time starts to stack up. A tactic to handle this challenge is to reduce the number of HTTP requests by bundling components.
How to reduce HTTP requests
In order to minify code in ASP.NET Core, we need to install a tool via NuGet. In this example, we are using the WebOptimizer.
Add the following to “program.cs”:
builder.Services.AddWebOptimizer(pipeline => {
pipeline.AddJavaScriptBundle(
"~/js/bundle.js", // Route
"/lib/jquery/dist/jquery.min.js",
"/lib/bootstrap/dist/js/bootstrap.bundle.min.js",
"/js/site.js"
);
pipeline.AddCssBundle(
"~/css/bundle.css", // Route
"/lib/bootstrap/dist/css/bootstrap.min.css",
"/css/site.css"
);
});
Add the following to our HTML file:
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
Here, we bundle the JavaScript files into bundle.js and CSS files into bundle.css. If we are using WebOptimizer to bundle the files, then it also minifies them as part of the process.
Remember to add the “app.UseWebOptimizer();” to the “Program.cs” file if you have not already done so.
Bonus info: While this tactic focuses on CSS and JS, it is also possible to reduce HTTP requests for images using a method called Image Sprites.
Browser Caching
Reducing HTTP requests definitely helps improve page speed, but if we can avoid them altogether, that would be even better. If we invoke the browser’s caching mechanism, we can store our components locally on the client and thereby avoid subsequent requests.
If you fire up your favorite browser developer tool and inspect one of the requests from your website, you will most likely find that the response contains an “ETag” and “Last-Modified” header, which helps us achieve our objective to some extent.
ETag (Entity Tag) - A unique identifier (e.g., hash) for a specific version of a given component. On subsequent requests, the browser will parse the ETag, and the server will either respond with an updated version if the component has changed or a “304 Not Modified,” telling the browser to use the cached version. In the best case, we save bandwidth (but still make a request)
Last-Modified - A timestamp indicating when a given component was last changed. On subsequent requests, the browser will parse the timestamp to check if a new version exists. The server will either respond with an updated version if the component has changed or a “304 Not Modified,” telling the browser to use the cached version. In the best case, we save bandwidth (but still make a request)
While both response headers are useful, they do not avoid subsequent requests, which would be the ideal case. In order to achieve this objective, we can apply another response header called “Cache-Control”, which we can use to tell the browser how long it can store and use a given component before it needs to retrieve it again from the server.
How to implement Cache-Control
In order to implement Cache-Control, we need to install a tool via NuGet. In this example, we are using WebOptimizer.
Add the following to the appsettings.json:
"webOptimizer": {
"enableCaching": true
}
A small bonus of using WebOptimizer for cache control is that it also automatically enables Cache Busting, which is useful should we need to update the browser’s cache with a newer version of our component. This is achieved by adding the ETag as a parameter to the URL query. If the ETag changes, then it forces the browser to retrieve a newer version of our component. Here are examples of the generated HTML for our CSS bundle with the ETag added to the query:
<link rel="stylesheet" href="/css/bundle.css?v={etag}" />
We could also add the above configuration to appsettings.development.json but there is no real advantage to use for local development. If anything, it might actually be a bit annoying if you are working on your frontend code.
Conclusion
Optimizing page speed in ASP.NET Core is essential for providing a seamless user experience and improving your website’s overall performance. By implementing file compression, minifying your code, reducing HTTP requests, and leveraging browser caching, you can ensure that your web pages load as quickly as possible.
Integrating these best practices into your ASP.NET Core applications not only improves the user experience but also boosts your SEO rankings, resulting in more traffic and better engagement. Start optimizing your pages today and reap the benefits of a faster, more efficient website!