Udostępnij za pośrednictwem


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.

Comments

  • Anonymous
    August 16, 2011
    I need to use both Razor and WebForms view engines in an MVC 3 application. I have done all the views in Razor except for a specific group of views that are used for some data entry.  All the data entry views are going through the same controller and have very similiar urls. Is there a way I can optimize the view engine look ups so that only requests to the specific urls/controller search for the web forms views, and all the other requests just search razor views and use the razor view engine? thanks marc If you need both view engines then you should continue having both of them registered. It's not like having only one registered will suddenly make your site run twice as fast. It will be on the order of a percent here or there (maybe more if you heavily depend on Editor Templates). But, if you want to optimize you could wite your own delegating view engine that wraps the two real ones and calls into the right one based on the httpContext. I'm just not sure if that provide any benefits. As I always say, you should measure.

  • Anonymous
    August 16, 2011
    What about CachedDataAnnotationsModelMetadataProvider at ASP.NET MVC 3 Futures? weblogs.asp.net/.../using-the-features-of-asp-net-mvc-3-futures.aspx Yes, that guy will also help your performance, but it does not affect view lookups. Perhaps I'll blog more about it at some point in the future.

  • Anonymous
    August 18, 2011
    Very Good! @jquerybrasil

  • Anonymous
    August 19, 2011
    Great article. Is there a way to tell the Razor VE to only look for cshtml and skip vbcshtml? Thanks Yes, you could edit the VirtualPathProviderViewEngine.FileExtensions property.

  • Anonymous
    April 27, 2012
    When you state "The more you have the longer the lookups will take, especially if the view engine you are using is registered last" does this mean that the lookup will go through all view engines even if a match is found in a previous view engine? What is the default order they're registered...is Razor first? If so and we're using Razor, is there really  a need to remove the others if my point above is false? Thanks The view lookup goes through all the registered view engines in order, until it finds a match. Once it finds a match it is finished, so it will not keep going. However, since Razor is registered second (after ASPX) if you started using Razor exclusively you are paying a small price for the ASPX engine being there.

  • Anonymous
    April 30, 2012
    Thanks! Can you possibly point me to where in the source I can see where the view engines are registered? I couldn't find it. The default view engines are registered in the ViewEngines class.