Hundreds of renderings? Your first-page-load could be sloooow

In this post

Having many subfolders of MVC views could impact page-load time.

Helix-style Feature folders

In a Helix-style solution, it’s common to group your MVC views by feature:

 /Views/Navigation/Nav.cshtml
 /Views/Navigation/Secondary/SecondaryNav.cshtml
 /Views/News/Headlines.cshtml
 /Views/News/Ticker/NewsTicker.cshtml

Large solutions may see 50, 60, 70+ MVC views making up a single page. If these views are in separate subfolders, we’ve noticed a performance penalty.

Just Helix-style solutions?

No, definitely not. Any solution with many views in many subfolders. Sitecore or no-Sitecore.

When will this affect me?

Each time you deploy to a new folder (ie, D:\Web\Octopus-1.2.3.4\), a new Temporary ASP.NET Files folder is populated with JIT-compiled versions of your .cshtml files. Typically you can see slow first-page-load times after a new deployment.

The technical details

Shout out: Oleg Volkov’s blog details what is going on here: https://ogvolkov.wordpress.com/2017/02/28/slow-asp-net-mvc-website-startup-in-case-of-many-view-folders/. Thanks, Oleg!

The System.Web.Compilation.BuildManager class (https://referencesource.microsoft.com/#System.Web/Compilation/BuildManager.cs,1662) contains a method, CompileWebFile(..), which JIT compiles your .cshtml files. In a handy performance boost, CompileWebFile(..) will batch this compilation, working on an entire directory at a time. This means that having 100 views in a single directory will compile a lot faster than having 100 views in 100 directories.

How much slower?

We did some strikingly unscientific testing by including 400 Partial Views on a page.

400 Views in 1 Folder

 @Html.Partial("~/Views/A/001.cshtml")
 @Html.Partial("~/Views/A/002.cshtml")
 ...
 @Html.Partial("~/Views/A/400.cshtml")
  • Create new directory, deploy to this directory
  • IIS Reset
  • First page load: 58s

400 Views in 40 Folder

 @Html.Partial("~/Views/B/1/001.cshtml")
 @Html.Partial("~/Views/B/1/002.cshtml")
 ...
 @Html.Partial("~/Views/E/10/010.cshtml")
  • Create new directory, deploy to this directory
  • IIS Reset
  • First page load: 3m26s

What’s the solution?

We went with MVC View precompilation (using https://github.com/StackExchange/StackExchange.Precompilation) because moving all .cshtml files to a single directory wasn’t a viable option. This brings the compilation time back down for us, and first-page-load after a deployment is now under 1 minute (previously 7+!).

 

Returning JSON errors from Sitecore MVC controllers

ASP.NET MVC gives us IExceptionFilter, with which we can create custom, global exception handlers to apply to controller actions.

public class ExceptionLoggingFilter : FilterAttribute, IExceptionFilter
{
	public void OnException(ExceptionContext filterContext)
	{
		// filterContext now contains lots of information about our exception, controller, action, etc
		filterContext.Exception.Message;
		filterContext.Exception.StackTrace;
		filterContext.Controller.GetType().Name;
		filterContext.Result.GetType().Name;
		UserAgent = filterContext.HttpContext.Request.UserAgent;
	}
}

 

We can apply this filter to all Action methods, by adding our filter to the list of global filters:

public class FilterConfig {
	public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
		filters.Add(new ExceptionLoggingFilter());
	}
}

 

and wiring this up to our application in our Application_Start method:

FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

 

In Sitecore

As you may expect, Sitecore exposes this functionality as pipeline processors. Sitecore defined a custom IExceptionFilter implementation (see our snippet above) which kicks off the mvc.exception pipeline, passing along the ExceptionContext object.

As client developers, it is our job to create an appropriate processor to accept the ExceptionContext and do something with it. Let’s run through an example where we want to return a JSON representation of the error, loaded with as much useful information as possible.

For more reading on Sitecore controller actions returning JSON, have a look at John West’s post here: https://community.sitecore.net/technical_blogs/b/sitecorejohn_blog/posts/use-json-and-mvc-to-retrieve-item-data-with-the-sitecore-asp-net-cms

So, first up, create an empty handler class, which inherits from ExceptionProcessor:

public class JSONExceptionHandler :
	Sitecore.Mvc.Pipelines.MvcEvents.Exception.ExceptionProcessor
{
	public override void Process(Sitecore.Mvc.Pipelines.MvcEvents.Exception.ExceptionArgs args)
	{

	}
}

 

Create a Web.config include, to add this processor to the mvc.exception pipeline:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.exception>
        <processor type="Bleep.Handlers.JSONExceptionHandler, Bleep.Handlers"/>
      </mvc.exception>
    </pipelines>
  </sitecore>
</configuration>

 

Ok! Now our JSONExceptionHandler class will be called each time an exception occurs in MVC code. So, let’s grab all the detail we can from the ExceptionContext class and return it as JSON:

public override void Process(Sitecore.Mvc.Pipelines.MvcEvents.Exception.ExceptionArgs args)
{
	var filterContext = args.ExceptionContext;
 
	filterContext.Result = new JsonResult
	{
		JsonRequestBehavior = JsonRequestBehavior.AllowGet,
                  Data = new
		  {
    			Message = filterContext.Exception.Message,
    			StackTrace = filterContext.Exception.StackTrace,
    			Controller = filterContext.Controller.GetType().Name,
    			Result = filterContext.Result.GetType().Name,
    			UserAgent = filterContext.HttpContext.Request.UserAgent,
    			ItemName = args.PageContext.Item.Name,
    			Device = args.PageContext.Device.DeviceItem.Name,
    			User = filterContext.HttpContext.User.Identity.Name
		  }
	};
 
	filterContext.ExceptionHandled = true;
 
	// Log the error
	Sitecore.Diagnostics.Log.Error("MVC exception processing " 
                	+ Sitecore.Context.RawUrl, args.ExceptionContext.Exception, this);
}

 

This will produce a result such as:

ExceptionFilter2

Happy hacking!