Optimizing ASP.NET MVC view lookup performance

Earlier today Sam Saffron from the StackExchange team blogged about the performance of view lookups in MVC. Specifically, he compared referencing a view by name (i.e. calling something like @Html.Partial("_ProductInfo")) and by path (@Html.Partial("~/Views/Shared/_ProductInfo.cshtml")). His results indicate that in scenarios where a page is composed from many views and partial views (for example, when you are rendering a partial view for each item in a list) it’s more efficient to reference your views using the full path instead of just the view name.

I know that bit of code quite well and it seemed strange to me that such a performance difference would exist because view lookups are cached regardless of how you reference them. Specifically, once an application is warmed up both the Razor and WebForms view engines use the lookup parameters to retrieve the cached results. If anything the cache key produced from a view path is usually longer than the one produced from a view name but even that should not have a measurable impact.

While I cannot explain the differences that Sam is seeing (and since I know he is seeing them in production too I’m quite sure he has the application configured correctly for performance testing) I thought this would be a good opportunity to present the basics of MVC view lookup optimizations as well as a few additional techniques that people might not be familiar with.

Hopefully you won’t go replacing all of your view references with full paths. Using view names is still easier and more maintainable. And before you do any perf-related changes you should always measure your application. It might turn out that it is fast enough for your needs already.

Run in Release mode

You should always make sure that your application is compiled in Release mode and that your web.config file is configured with <compilation debug="false" />. That second part is super-important since MVC will not do any view lookup caching if you are running your application in debug mode. This helps when you are developing your application and frequently adding/deleting view files, but it will kill your performance in production.

This might seem like obvious advice, but I have seen even experienced devs get bitten by this.

Use only the View Engines that you need

I’ve mentioned this before but it’s worth repeating. The MVC framework supports having multiple view engines configured simultaneously in your application and will query each in turn when it is trying to find a view. The more you have the longer the lookups will take, especially if the view engine you are using is registered last. In MVC 3 we register two view engines by default (WebForms and Razor) and in all versions you might have installed 3rd party view engine such as Spark on nHaml. There is no reason to pay the performance price for something you are not using so make sure you specify only the view engines you need in your Global.asax file:

 protected void Application_Start() {
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new RazorViewEngine());
    ...
}

Customize the view lookup caching

By default (when running in Release mode, of course) MVC will cache the results of the lookups in the application cache available via HttpContext.Cache. While this cache works great and helps us avoid having to check for view files on disk there is also a cost associated with using it (this includes the cost of a thread-safe lookup as well as all the additional cache management such as updating entry expiration policies and performance counters).

To speed things up you could introduce a faster cache in front of the application cache. Fortunately all view engines deriving from VirtualPathProviderViewEngine (that includes WebForms and Razor) have an extensibility point via the settable ViewLocationCache property.

So we can create a new class that implements the IViewLocationCache interface:

 public class TwoLevelViewCache : IViewLocationCache {
    private readonly static object s_key = new object();
    private readonly IViewLocationCache _cache;

    public TwoLevelViewCache(IViewLocationCache cache) {
        _cache = cache;
    }

    private static IDictionary<string, string> GetRequestCache(HttpContextBase httpContext) {
        var d = httpContext.Items[s_key] as IDictionary<string, string>;
        if (d == null) {
            d = new Dictionary<string, string>();
            httpContext.Items[s_key] = d;
        }
        return d;
    }

    public string GetViewLocation(HttpContextBase httpContext, string key) {
        var d = GetRequestCache(httpContext);
        string location;
        if (!d.TryGetValue(key, out location)) {
            location = _cache.GetViewLocation(httpContext, key);
            d[key] = location;
        }
        return location;
    }

    public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath) {
        _cache.InsertViewLocation(httpContext, key, virtualPath);
    }
}

and augment our view engine registration in the following way:

 protected void Application_Start() {
    ViewEngines.Engines.Clear();
    var ve = new RazorViewEngine();
    ve.ViewLocationCache = new TwoLevelViewCache(ve.ViewLocationCache);
    ViewEngines.Engines.Add(ve);
    ... 
}

This TwoLevelViewCache will work best in views that call the same partial multiple times in a single request (and should hopefully have minimum impact on simpler pages).

You could go even further and instead of the simple dictionary stored in httpContext.Items you could use a static instance of ConcurrentDictionary<string, string>. Just remember that if you do that your application might behave incorrectly if you remove or add view files without restarting the app domain.