Optimizing First Load for MVC in Azure Web Roles

Azure DevOps release pipelines are awesome for continuous deployment of code from Git repositories to Azure web roles. However, after that update the first page load can be slow. In my case, very slow. The warm-up took on average 90 seconds.

Another great feature of cloud is hyper scale and automatic horizontal scaling. But again, I am worried about that initial page load.

To address this, let's first talk;

Precompiling MVC views and web assembly bundling

We can enforce compilation of all our Razr views and this helps tremendously for initial page load. This causes all cshtml pages to end up in the DLL assembly and the files will actually be empty. Normally, the JIT (just-in-time) compiler takes care of this and allows pages to be compiled after change. In our case, all views are compiled and controlled by our release pipeline so no need for JIT.

To enable precompilation, we need to add a compiler build argument as

/p:PrecompileBeforePublish=true

Another feature that can greatly speed up initial load is assembly bundling. This option merges several assemblies into one and therefore speeds up the initial load of the assembly into memory. This can be enabled using the compiler argument

/p:UseMerge=true /p:SingleAssemblyName=AppCode

We need to edit our DevOps release pipeline and add these build arguments as follows.

This is great for speeding up the initial load, but we also want to minimise the load time for the first user so let's:

Schedule warm-up as part of your release pipeline

We can get the pipeline to warm up the site as part of the release as well by simply adding a PowerShell step as:

$WebResponse = Invoke-WebRequest "https://mywebroleaddress.azurewebsites.net/anonymous-controller"

Note that it is important to fetch an anonymous controller so that the call is not blocked externally.

This is added as a  new step in the release pipeline.

However, we still may get a performance hit if my application scales. So let's:

Ensure warm up jobs are completed during scaling in Azure

First thing to do is a bit of Azure configuration. Ensure that you have set the Always On option for the web role as this stops unwanted app pool recycles and prevents cooling down the web role. You can find it under the configuration tab in the web role.

Then, we enable scale out to increase the instance count and add a web role to the load balancer. This example scales out when the CPU is above 80% load.

However, when the app scales then we will get a warmup issue and a wait. So we need to update our code by

  • Installing Azure SDK (part of Visual Studio, use the standard Visual Studio installer)
  • Add a reference to Microsoft.WindowsAzure.ServiceRuntime
  • Add a new class to your project and inherit from RoleEntryPoint

The RoleEntryPoint class is called by Azure runtime during startup. We can then call to do specific tasks during that load. I fetch large objects from the database and cache them in server memory for example:

 

using System;
using System.Collections.Generic;
using Microsoft.WindowsAzure.ServiceRuntime;

namespace MyProject.Web
{
    /// <summary>
    /// This class is called by Microsoft Azure during service provisioning and warms up the new instance.
    /// </summary>
    /// <seealso cref="Microsoft.WindowsAzure.ServiceRuntime.RoleEntryPoint" />
    public class WebRole : RoleEntryPoint
    {
        /// <summary>
        /// Called when the application starts after a provisioning event.
        /// </summary>
        /// <returns>A <see cref="bool"/> stating that the service has started.</returns>
        public override bool OnStart()
        {
            try
            {
                // Get cached data for all our static instances. 
                // NOTE: Actual code removed for security purposes, illustration only
                decimal instanceCount = SampleModel1.StaticInstance.Count;
                instanceCount += SampleModel2.StaticInstance.Count;

                // etc etc...
            }
            catch (Exception e)
            {
                ErrorHandler.Raise(e);
            }

            return base.OnStart();
        }
    }
}

Conclusion

All of these changes made a dramatic impact for me. I went from a startup time of 90 seconds to under 10 seconds. And that is if I managed to get the site before the release manager warmup was completed. In other cases, the warm up was completely gone.