Within this tutorial, you will learn how to tweak your Umbraco v10 (or v9) website so that it runs blazingly fast πŸƒπŸ’¨πŸƒπŸ’¨Configuring performance within .NET Core is very different compared to .NET Framework. Within .NET Core the majority of performance enhancements are enabled through middleware.

If you are using .NET 5 you will be enable this middleware within Startup.cs. If you using .NET 6 then you can enable these middleware directly within Program.cs. For this tutorial, I will use Startup.cs so this tutorial will apply to most amount of readers, however, pick the location that makes you happy ❀️

When it comes to improving your overall performance metrics within a Umbraco website, you will want to target several types of performance optimizations. These include:

  • Page-level caching
  • View-level caching
  • Minification and bundling
  • Asset-level caching
  • Object-level caching

Within this guide, we will cover at least one strategy for each technique, so if you want to become a performance master, read on πŸ”₯πŸ”₯πŸ”₯

Client-side Caching

In this first caching technique, you will add certain cache headers to incoming page requests. When set, these headers will tell your website visitor's browser to cache your pages for as long as the expiry time is set. After a site visitor has visited a page on your site, all subsequent access to that page will use the browsers' locally cached version. As you would expect, this type of caching can drastically reduce the number of calls made to your server.

The steps to enable client-side caching within .NET Core are very similar to most of the other techniques you will read about in this section. You will need to enable some middleware within Startup.cs (or just `Program.cs in .NET 6) and add a special attribute onto any controller that you want to be cached. Below shows an example of how a cache header would look:

This specific header will tell the browser to cache the response for 90 seconds. After 90 seconds have elapsed, on subsequent visits the web browser will request a new version of the page from the server rather than use its local copy.

When enabling client-side caching, determining the optimal cache time for a page can be very tricky and will need some consideration. Making the caching period too long will mean your site visitors might miss out on important news and content updates. You will need to consider how often your pages update. Typically, you will want to set an expiry time somewhere between a few hours to a few days. To enable client-side caching the first step is to enable some middleware within Startup.cs for .NET 5 (or Program.cs for .NET 6):

This specific header will tell the browser to cache the response for 90 seconds. After 90 seconds have elapsed, on subsequent visits the web browser will request a new version of the page from the server rather than use its local copy.

When enabling client-side caching, determining the optimal cache time for a page can be very tricky and will need some consideration. After a browser has a cached version of a page, it will not re-request that page from your server until the cache duration has been met. There is no way to force the client's browser to re-call your server after a page is cached. Once cached, the only way for a client to see any content updates is to wait. If you enabled a one-year cache on your homepage, no matter how many updates you make to it, the client will not see those changes until they either clear their local browser cache or the cache duration has expired.

Caching pages that change frequently with a very long cache duration will mean your site visitors might miss out on important news and content updates. This is why when you use client-side caching, you will need to consider how often your pages update. Typically, you will want to set an expiry time somewhere between a few hours to a few days. To enable client-side caching the first step is to enable some middleware within Startup.cs for .NET 5 (or Program.cs for .NET 6):

After enabling the middleware your next step is to define how long the browser should cache each page. You will need to apply these settings on a per request basis, meaning you will need to add some code at the controller level. You will need to decorate all of the page controllers that you want to be cached with the ResponseCache attribute.

To use this attribute, you may need to install the Microsoft.AspNetCore.ResponseCaching NuGet package. The ResponseCache attribute will allow you to determine what gets cached and for how long. You can configure how the page is cached through six very useful properties:

  • Duration
  • CacheProfileName
  • Location
  • NoStore
  • VaryByQueryKeys
  • VaryByHeader

For the bare-bones cache configuration, you need to define the cache expiry time. This is done by specifying either a cache duration or a cache profile. For best practice, you should favour using profiles. Without using a profile, you will have to duplicate your cache settings within your controllers. This will violate the DRY principle which is not ideal. You can set up a cache profile within the middleware like this:

You can then add a reference to the profile from ResponseCache like this:

With your profile defined, you should also consider how your pages can be rendered. If the page can be rendered in different states, you may need to cache multiple versions of that page. For example, if you use personalization you will not want any personalized content to be cached. Whether it be changing content based on anonymous/member content, personalized content, or whatever else you will need to add additional configuration. When you need to consider caching and page versions, I recommend that you research the other properties exposed by the ResponseCache attribute. By tweaking these settings you can get a very fine-grain level of control over how the page is cached.

On each page request, the middleware takes the cache duration defined within the ResponseCache attribute from the controller and add the correct headers. The response cache will also respect the VaryByHeader and VaryByHeader options, allowing for additional versions of a page to bypass the cache.

Server-side Caching

Traditionally, the quickest way to enable server-side caching within .NET Framework was to enable the output cache. Unfortunately, life is not so easy when it comes to .NET Core development. As a .NET Core application can run on any environment, having a single in-memory cache that works between Mac, Windows, or Linux is not as straightforward as it used to be. Instead, to get a similar capability within .NET Core you will need to turn to a third-party package.

The package that I have successfully used on my projects is called WebEssentials.AspNetCore.OutputCaching. The WebEssentials AspNetCore.OutputCaching package was written by Mads Kristensen, a dude who has written a lot of useful .NET utilities. To enable the output cache you will need to enable the output cache middleware within Startup.cs (or Program.cs for .NET 6), like this:

Once enabled, this middleware will read the cache duration set within the responses cache-control header. The way in which you add the cache headers to a request is the same as in client-side testing. You will need to decorate your controllers (or actions) with the OutputCache attribute:

After decorating your controllers with the OutputCache attribute, at run-time, when a page request is made and the corresponding controller is triggered, the WebEssentials output cache plug-in will read the controller's cache configuration and cache the page appropriately. The OutputCache attribute also works based on the cache expiration time. You will need to define either a Duration or a Profile. Profiles are defined within Startup.cs (or Program.cs for .NET 6). The process is very similar to the client-side process!

View-level Caching

To ensure that a page's HTML is generated as quickly as possible, you should try to cache as much of the page creation process as possible. As mentioned at the start of the book, within .NET Core a view can be broken into smaller chunks using something called a view component. One reason why view components improve page load time is due to their asynchronous nature. When used on a page, the server can parse multiple view components in parallel making the creation process slightly quicker.

View components can also be cached. Instead of making the server have to completely regenerate the contents of a view component each time, caching its output will further speed things up.

Caching view components is possible by using the caching Tag helper. Caching a view component using this helper is pretty simple, just wrap the components tag deceleration using the cache tag helper like this:

The cache tag helper will also allow you to vary what gets cached by many different-criteria including header values (vary-by-header), query-string value (vary-by-query), routing (vary-by-route), cookie (vary-by-cookie), and user (vary-by-user). A complete list of how the tag helper can be configured can be found here. On your project I recommend you add your site's header and footer into their own view components. Caching both of these view components is an easy win that will improve performance on all pages!

Asset level Caching

Web pages are comprised of more than just HTML. When creating a page, content editors will also likely want to enhance the user experience by linking to images, eBooks, videos, white papers, webinars and a whole host of other interactive content.

Simply uploading images and files within the Umbraco media library will not cache them. You will need to apply some additional asset-level caching configuration yourself.

Failing to add this configuration will mean you will encounter two performance niggles. The first issue will be at the CDN level (assuming that you are using one). When your assets are not set with the correct cache headers, your CDN will route all traffic back to origin. The consequence of this is that all your site visitors from around the globe will need to route back to wherever your server is located. Adding additional distance to a request will result in slower page load times.

The second issue is around capacity. As your customers will be making more requests back to your server, that server's total throughput will be reduced. The busier your server becomes, the less traffic will be able to access your site. To improve performance you should enable asset caching. To do this, you will again need to enable some middleware and add some configuration within Startup.cs.

You should enable the StaticFileMiddleware early on within Configure() by making a call to AddStaticFiles(). This configuration needs to be at the top of the method because adding it towards the end of Configure() can result in assets not being cached correctly

After adding this configuration to your application, images and files added by the content editors to the Umbraco media library should then be served with an appropriate cache header.

To check the correct headers have been applied, load the asset within a browser and then check the header values within the network request tab. Check the headers contains an entry that looks like this:

cache-control: public, max-age=432000

Before you start caching assets, you need to consider the cache expiration period. The good thing with asset caching is that determining how long to cache an asset is usually much easier compared to page-level cache times.

After an image gets uploaded and cropped it is highly unlikely that the asset will ever change. If it does, it is much more likely that a new image will be uploaded and linked to instead. This means that when applying caching onto static assets you can usually use a much high cache expiration time compared to page-level caching.

Using a technique called cache-busting, you can even set the cache duration to infinity if you really wanted to. In cache busting, a postfix is appended onto the assets URL. This cache identifier is usually calculated based on the asset's publish time and date. Updating the page HTML to point to the original asset with a new identifier will force the browser to re-fetch the asset from the server.

Cache busting is a topic that all web developers need to understand. If this topic is new to you, I suggest you spend some time researching it. A good tutorial to learn more about this subject can be found within the further reading section. Combining smaller page-level cache times with cache busting and extremely long asset cache times will give you the best performance results.

You can enable asset-level caching using the StaticFileMiddleware within Startup.cs. Just like page caching, when an incoming request for an asset is made, the middleware will add the correct caching headers to the response.

Minification and Bundling

Another strategy to speed up page performance is to ensure that all your site's CSS and Javascript files are minified. Minification and bundling will ensure clients load any CSS and Javascript your site uses as fast as possible.

Bundling can be done either frontend or server-side. Depending on your front-end developers' preference, they may opt to use something like Vite or Web Pack to do the bundling, however, bundling is also possible using C#.

When you originally installed Umbraco, behind-the-scenes an additional third-party NuGet package called Smidge was also installed.

Smidge, written by Shannon Deminick, is a CSS and JavaScript file minification, combination, compression and management library for ASP.Net Core. Although Smidge is used by the Umbraco backend to improve page load time, you are also free to use Smidge to bundle your custom JS and CSS files.

Smidge can be used either within a vanilla .NET Core website or within a Umbraco-powered website. As Smidge is included within Umbraco, most of the installation steps have already been applied. You can configure how Smidge works by adding some JSON within the RuntimeMinification setting within appsettings.json:

To define which JS and CSS files Smidge should bundle, you will need to enable the Smidge middleware and then reference the files you want to include. This is done within Startup.cs (or Program.cs in .NET 6).

To access the URLs to the bundled Javascript files, you can use this code?

The reference js-script was defined within the configuration inside of Startup.cs. By passing in the bundle name, Smidge will then render things appropriately.

You can also repeat the same process to render the CSS bundle. In this example, the bundle is called css-script. Use this code to access the render the minified CSS:

Object-level caching

Asides from page-level and HTML-level caching, you should also cache the output of any processor-intensive or time-consuming operations. Querying an API for data, reading data from disk, reading pricing data from a PIM, and accessing commonly used settings are all examples of things that might be better off cached. By introducing caching around these calls, you can also increase your site's reliability.

Out-of-the-box, .NET Core provides two APIs to allow for caching, IMemoryCache and IDistributedCache. The difference between these two caching helpers is where the cached data will be stored. Using the in-memory cache helper will add the cache data within the application's server memory. Whereas within a distributed cache, data is stored within an external service that multiple servers can access.

Out of the two, in-memory caching is definitely the simpler cache to implement. The main downside of this type of caching is server impact. Since in-memory caching uses the current environment's RAM, you should treat object caching as a scarce resource. If you max out your server's RAM, expect bad things to happen. A good practice is to limit the number of items you add to the cache and always use a cache size limit.

If you need to make use of a distributed cache, I would recommend that you also consider implementing something like Redis, or another NoSql option before just jumping into IDistributedCache. Using the .NET Core distributed cache helper is well documented here.

The in-memory cache helper does rely on some middleware for it to work, however, this middleware should be automatically enabled when you install Umbraco. If you have issues getting and retrieving items from the cache, you might want to manually add that middleware within Startup.cs using the AddMemoryCache() activation method.

To add an item within the cache, you need to inject the IMemoryCache API into your code using dependency injection. To access a value from the cache, you can use the helpers TryGetValue() method. TryGetValue() takes a cache key and if a corresponding item in the cache exists, it will return the corresponding value back:

Adding items to the cache is done using the Set() method. When adding items to the cache, it is also possible to define when the item should be removed from the cache. Adding this cache duration is important to prevent the server's RAM from filling up. To add the cache expiration policy you need to pass in an additional options object into Set():

MemoryCacheEntryOptions provides four different types of configuration:

  • SlidingExpiration – defines how long a cache entry can be inactive before it is removed from the cache
  • AbsoluteExpiration – defines an exact date when an item is removed from the cache
  • Priority – defines how important that cached object is. A higher priority object will be retrieved first
  • Size – defines the size limit that entry

It is also possible to manually remove an item from the cache in code using Remove():

After combining these four types of caching strategies, you should see a noticeable improvement in your application's performance. After you have the basics, it is then a case of testing your page speed and tweaking until things are dialled in.

There are many different types of performance tweaks that you can apply to your website. This tutorial is not an exhaustive list, however, it does cover enough to ensure your website loads quickly.

To get the best possible performance you will want to use all of the techniques listed within this guide. Performance testing is a trial and error process. You need to apply all of these strategies and then use tools like Page Insights and Lighthouse in order to dial in the settings and ensure ultimate performance.

Good luck and happy coding 🀘