Warming-Up Umbraco for Peak Performance

Explore how to enhance Umbraco CMS performance with a warm-up process, including creating a hosted service, a strategy for preloading data, and dynamic content. We will cover implementing efficient URL warming strategies for improved response times and user experience.

What is Umbraco warm-up?

The Umbraco warm-up process entails preparing an application for optimal performance before serving live traffic.

This is particularly important when we experience a cold start, which can lead to slower response times when the app is accessed after being idle or newly deployed.

Warming up an app involves executing tasks that ensure all necessary components are fully operational and running efficiently.

Why use Background Job for Umbraco warm-up?

Using a background job for warming up an Umbraco CMS is strategic because it prepares your application for peak performance without impacting the main thread's responsiveness.

This approach allows resource-intensive tasks, such as preloading content, compiling pages, and warming up cache, to be handled in the background. 

By doing so, the main thread remains free to handle incoming user requests immediately, ensuring a smooth and fast user experience from the very start. 

Alternatively, you can use the UmbracoApplicationStartedNotification handler to warm up a website, but you must be aware that this solution may not be efficient in some scenarios.

Introducing the Warm-Up Service

The UmbracoWarmerHostedService operates by scheduling and executing a background task that pre-loads specified URLs of an Umbraco site.

It initiates a scope to access services that provide a list of URLs to warm up and a strategy to warm them up, typically by making HTTP requests to these URLs.

This ensures the site's pages are cached and ready for quick delivery to visitors.

The service logs the outcome of each warm-up attempt, allowing for monitoring and troubleshooting of the warm-up process. 

public class UmbracoWarmerHostedService : RecurringHostedServiceBase
{
    private readonly ILogger<UmbracoWarmerHostedService> _logger;
    private readonly IServiceProvider _serviceProvider;
    private static TimeSpan _period = TimeSpan.FromDays(1); //Let's assume one warm up daily
    private static TimeSpan _delay = TimeSpan.FromSeconds(60);
    private IUrlWarmupStrategy _urlWarmupStrategy;
    private IUrlWarmupProvider _urlWarmupProvider;
  
    public UmbracoWarmerHostedService(
        ILogger<UmbracoWarmerHostedService> logger,
        IServiceProvider serviceProvider) : base(logger, _period, _delay)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
    
    public override async Task PerformExecuteAsync(object? state)
    {
        using (var scope = _serviceProvider.CreateScope())
        {
            _urlWarmupProvider = scope.ServiceProvider.GetRequiredService<IUrlWarmupProvider>();
            _urlWarmupStrategy = scope.ServiceProvider.GetRequiredService<IUrlWarmupStrategy>();
            
            var urls = await _urlWarmupProvider.GetWarmupUrlsAsync();
         
            foreach (var url in urls)
            {
                bool isSuccess = await _urlWarmupStrategy.WarmupUrlAsync(url);

                if (isSuccess)
                {
                    _logger.LogInformation($"{url} was warmed up successfully");
                }
                else
                {
                    _logger.LogError($"{url} warm up failed");
                    
                    // Handle the failure case, e.g., log, retry, or take other actions
                }
            }
        }
    }
}

Once the background job executes - you can expect a similar trace log:

[10:02:35 INF] http://example.com/page1/ was warmed up successfully
[10:02:36 ERR] http://example.com/page2/ warm up failed
[10:02:36 INF] http://example.com/page3/ was warmed up successfully
[10:02:36 ERR] http://example.com/page4/ warm up failed

The Core Interfaces: IUrlWarmupStrategy and IUrlWarmupProvider interfaces

These define contracts for developing strategies to warm up URLs and for providing URLs to be warmed up, respectively.

public interface IUrlWarmupStrategy
{
    Task<bool> WarmupUrlAsync(string url);
}
public interface IUrlWarmupProvider
{
    Task<IList<string>> GetWarmupUrlsAsync();
}

Static and Dynamic URL Warm-Up

The StaticUrlWarmupProvider allows for the specification of a predefined list of URLs to be preloaded, ideal for consistently crucial pages of your site.

Conversely, the DynamicUrlWarmupProvider dynamically generates URLs to warm up based on the Umbraco content types you need, adapting to site content changes.

This flexibility means you can easily switch between static and dynamic strategies based on your site's needs, ensuring optimal performance and user experience by keeping essential pages ready for immediate access.

public class StaticUrlWarmupProvider : IUrlWarmupProvider
{
    public async Task<IList<string>> GetWarmupUrlsAsync()
    {
        // This example returns a static list of URLs, but you could
        // fetch them from a database, configuration file, or external service.
        return new List<string>
        {
            "http://example.com/page1",
            "http://example.com/page2"
        };
    }
}
public class DynamicUrlWarmupProvider : IUrlWarmupProvider
{
    private readonly PublishedContentHelper _publishedContentHelper;
    private readonly IList<string> _contentTypesAliasesToWarmup;

    public DynamicUrlWarmupProvider(PublishedContentHelper publishedContentHelper)
    {
        _publishedContentHelper = publishedContentHelper;
        
        // Initialize with a predefined list of content type aliases to warm up
        _contentTypesAliasesToWarmup = new List<string>
        {
            Home.ModelTypeAlias,
            LandingPage.ModelTypeAlias,
            // Add more content type aliases as needed
        };
    }
    
    public Task<IList<string>> GetWarmupUrlsAsync()
    {
        List<string> urls = new List<string>();
        
        foreach (var contentType in _contentTypesAliasesToWarmup)
        {
            var contentUrls = _publishedContentHelper.GetAllUrlsByContentType(contentType);
            
            urls.AddRange(contentUrls);
        }
        
        return Task.FromResult((IList<string>)urls);
    }
}

To get all Url's for specific content types in Umbraco, you can implement a similiar helper:

public class PublishedContentHelper
{
    private IUmbracoContextFactory _umbracoContextFactory;
    
    public PublishedContentHelper(IUmbracoContextFactory umbracoContextFactory)
    {
        _umbracoContextFactory = umbracoContextFactory;
    }
    
    public IList<string> GetAllUrlsByContentType(string alias)
    {
        using (var cref = _umbracoContextFactory.EnsureUmbracoContext())
        {
            var contentCache = cref.UmbracoContext.Content;

            var contentType = contentCache.GetContentType(alias);

            if (contentType == null) throw new ArgumentNullException($"Published content type not found for {alias}");

            return contentCache.GetByContentType(contentType).Select(x=>x.Url()).ToList();
        }
    }
}

Accelerating Umbraco with the UrlWarmupHttpClientStrategy

The UrlWarmupHttpClientStrategy uses an HttpClient to send HTTP requests to URLs that need to be preloaded.

Key parameters include the HttpClient for making requests and a logger for tracking the outcome of each attempt. 

The strategy sets a request timeout to avoid long waits and handle successful responses and failures.

public class UrlWarmupHttpClientStrategy : IUrlWarmupStrategy
{
    private readonly HttpClient _httpClient;
    private TimeSpan _requestTimeout = TimeSpan.FromSeconds(90);
    private readonly ILogger<UrlWarmupHttpClientStrategy> _logger;

    public UrlWarmupHttpClientStrategy(HttpClient httpClient, ILogger<UrlWarmupHttpClientStrategy> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        
        _httpClient.Timeout = _requestTimeout;
    }

    public async Task<bool> WarmupUrlAsync(string url)
    {
        try
        {
            // Attempt to make the HTTP call
            var response = await _httpClient.GetAsync($"{url}?warmup=1");
            
            // Check if the response indicates success (e.g., HTTP 200 OK)
            if (response.IsSuccessStatusCode)
            {
                // Log success or store success state as needed
                return true; // Indicates the refresh was successful
            }
            else
            {
                // Log failure or store failure state as needed
                return false; // Indicates the refresh was not successful
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Warm up failed for {url}.");
            
            // Log exception details here
            return false; // Indicates the refresh was not successful due to an exception
        }
    }
}

Integrate Warm-Up Services in Umbraco

You must configure an Umbraco application to use a warm-up service and dynamic and static URL providers. This is done within the Compose method.

The commented line shows how to switch to a static list of URLs by using the StaticUrlWarmupProvider. 

public void Compose(IUmbracoBuilder builder)
{
	builder.Services.AddHostedService<UmbracoWarmerHostedService>();
	builder.Services.AddScoped<IUrlWarmupProvider, DynamicUrlWarmupProvider>();
	builder.Services.AddScoped<IUrlWarmupStrategy, UrlWarmupHttpClientStrategy>();
	//builder.Services.AddScoped<IUrlWarmupProvider, StaticUrlWarmupProvider>();
}

Final Thoughts: Energizing Umbraco for Speed and Efficiency

To make your Umbraco site fast and efficient, focus on warming it up.

Use dynamic and static content providers along with smart HTTP strategies.

By preloading content, you ensure your site is ready and performs at its best when users arrive.

🌐 Explore More: Interested in learning about Umbraco and web development insights?

Explore our blog for a wealth of information and expert advice.

↑ Top ↑