WARNING

Pretty much all the results / conclusions here are off. Turns out ASP.NET MVC has some pretty aggressive caching that is disabled in certain conditions:

protected VirtualPathProviderViewEngine() {
    if (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled) {
        ViewLocationCache = DefaultViewLocationCache.Null;
    }
    else {
        ViewLocationCache = new DefaultViewLocationCache();
    }
}

HttpContext.Current.IsDebuggingEnabled will be set to true if your web.config contains

<configuration>
   <system.web>
     <compilation debug="true"  ...

This is totally unrelated to you compiling your project in release or not.

If there is one takeaway point here it is that you should have an admin stats page in your web app that draws a big red box somewhere if HttpContext.Current.IsDebuggingEnabled is set to true.

Unfortunately, when I ran my tests for this blog post in release mode, the default web project did not amend the compilation node. Leading to these rather concerning and erroneous results.

I just deployed the big red box™ to all our admin pages in the SE network and confirmed we are not running with compilation debug … so I blame my entire analysis below on a poor test setup.

The results below are only relevant to DEBUG mode


A few months ago I was fighting some performance dragons at Stack Overflow. We had a page that renders a partial per answer and I noticed that performance was leaking around the code responsible for locating partials. The leak was tiny, only 0.3ms per call - but it quickly added up. I tweeted the guru for some advice and got this:

tweet

You see, Phil has seen this happen before. I am told the code that locates partials in the next version of ASP.NET MVC is way faster.

However, for now, there are some things you should know.

###How long does it take to render a view?

Rendering views in ASP.NET MVC is a relatively straight forward procedure. First, all the ViewEngines are interrogated, in order, about the view in question using the FindView or FindPartialView methods the ViewEngines provide. The view engine may be interrogated twice, once for a cached result and another time for a non-cached result.

Next, if a view is found Render is called on the actual IView.

So, the process breaks down to view location and view rendering. The logical separation is quite important from a perf perspective. Finding can be fast and rendering slow or vica versa.

As it turns out the performance of your view location heavily depends on how explicit you are with your view name.

This matters less if you only have a single view to find, however often you may have hundreds of views to locate during construction of a single page, leading to pretty heavy performance leaks.

###An illustrated example

Take this simple view.

@for (int i = 0; i < 30; i++)
{
    @Html.Partial("_ProductInfo",  "Product" + i )
}

@for (int i = 0; i < 30; i++)
{
    @Html.Partial(@"~/Views/Shared/_ProductInfo.cshtml",  "Product" + i )
}

When we enable profiling for this page we get some pretty interesting results:

first

VS.

second

The first sample, which happens to be the cleaner and easier to maintain code comes with a 18ms perf penalty.

On an interesting side-note non-partials are effected as well:

\\ 0.6ms faster than return View();
return View(@"~/Views/Home/Index.cshtml");

###Does this issue affect me?

I recently added FindView profiling to MVC Mini Profiler. This will allow you to quickly determine the impact of view location within your page. This is done by intercepting the ViewEngines.

For those who want a look at the code, this is the trick I use:

private ViewEngineResult Find(ControllerContext controllerContext, string name, Func<ViewEngineResult> finder, bool isPartial)
{
    var profiler = MiniProfiler.Current;
    IDisposable block = null;
    var key = "find-view-or-partial";

    if (profiler != null)
    {
        block = HttpContext.Current.Items[key] as IDisposable;
        if (block == null)
        {
            HttpContext.Current.Items[key] = block = profiler.Step("Find: " + name);
        }
    }

    var found = finder();
    if (found != null && found.View != null)
    {
        found = new ViewEngineResult(new WrappedView(found.View, name, isPartial: isPartial), this);

        if (found != null && block != null)
        {
            block.Dispose();
            HttpContext.Current.Items[key] = null;
        }
    }

    if (found == null && block != null && this == ViewEngines.Engines.Last())
    {
        block.Dispose();
        HttpContext.Current.Items[key] = null;
    }

    return found;
}

The new “extra” bit of profiling will be available in the next version of Mini Profiler.

Epilogue

One very important take away is that every view engine in the pipeline can add to your perf impact. If most of your views are Razor, reordering the pipeline so you process Razor views first will result in some nice performance improvements. If you are not using a View Engine be sure to remove it from the pipeline.

Here is a screenshot of the default view engines (notice that WebFormsViewEngine is first):

default view engines

View Engines are interrogated twice, once for cached views and a second time for uncached views.

For example to only include Razor you could add:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());

Happy profiling.

Comments

Kenny_Eliasson over 12 years ago
Kenny_Eliasson

Found out about this problem when I was looping over collection with 100+ items, rendering a partial for each item. Made an enormous difference to change to ~.

Marcin_Dobosz over 12 years ago
Marcin_Dobosz

Sam, another thing you could try is to implement your own IViewLocationCache. See my blog post for more details: http://blogs.msdn.com/b/marcinon/archive/2011/08/16/optimizing-mvc-view-lookup-performance.aspx

I'm still a bit suspicious of your results. In a default MVC application that's been warmed up the second round of calls to the view engines should not happen (assuming you never request views that don't exist). Are you perhaps using Editor Templates (via EditorFor etc?). Or do you guys do something different with the view engine lookups to support mobile views?

Sam Saffron over 12 years ago
Sam Saffron

@Marcin confirmed … I had a shoddy setup when writing this blog post

Alexander_Gornik over 12 years ago
Alexander_Gornik

By the way, you can force turn asp.net debug setting in production by using machine.config files on prod servers:

http://weblogs.asp.net/scottgu/archive/2006/04/11/442448.aspx

Abhaysingh Rajpurohit almost 8 years ago
Abhaysingh Rajpurohit

@haacked, I am getting this error for @Html.Partial(@"~/Views/Shared/_Top_Acq_Details")

The partial view ‘~/Views/Shared/_Top_Acq_Details’ was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Shared/_Top_Acq_Details

File is there in Views/Shared/_Top_Acq_Details

Sam Saffron almost 8 years ago
Sam Saffron

@haacked has moved on from then :slight_smile:

I recommend you continue this discussion on Stack Overflow :ice_cream:

Sam Saffron almost 8 years ago
Sam Saffron

comments powered by Discourse