Oh view where are thou: finding views in ASP.NET MVC3
over 13 years ago
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:
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:
VS.
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):
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.
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 ~.