I read a blog post earlier this week that talked about the benefits of compiling your View files to increase performance in Sitecore applications. Reading that post (which
I stupidly failed to keep track of the link to, so can't reference it now
the comments pointed me back to) reminded me of an interesting issue that came up on a project I was looking at recently. If you're interested in the raw performance of your Sitecore sites, you might want to consider this as well when you're planning your views:
A client was running some detailed performance testing on a site. Along with some other issues, they noticed that their performance trace data showed a surprising amount of time spent in
RazorGenerator.Mvc.PrecompiledMvcEngine.FileExists
method. For simple page requests they would see trace data including lines like this:
On some pages about 200ms of the overall execution time was accounted for by calls to this method, which seems a really large amount of time for framework code like this. The code was running on servers with SSD drives, so it was unlikely to be a physical disk access issue. Since they were quite focused on the performance of their pages, the client was interested in what was happening to cause this, and what could be done about it.
protected override bool FileExists(ControllerContext controllerContext, string virtualPath) { virtualPath = PrecompiledMvcEngine.EnsureVirtualPathPrefix(virtualPath); ViewMapping mapping; if (!_mappings.TryGetValue(virtualPath, out mapping)) { return false; } if (mapping.ViewAssembly.UsePhysicalViewsIfNewer && mapping.ViewAssembly.IsPhysicalFileNewer(virtualPath)) { // If the physical file on disk is newer and the user's opted in this behavior, serve it instead. return false; } return Exists(virtualPath); }
If the setting for
UsePhysicalViewsIfNewer
is true and the view file is newer then the logic will make use of the cshtml file instead of the data in the assembly when the view file is newer. The test for the age of the cshtml file takes place in this method:
internal static bool IsPhysicalFileNewer(string virtualPath, string baseVirtualPath, Lazy<DateTime> assemblyLastWriteTime) { if (virtualPath.StartsWith(baseVirtualPath ?? String.Empty, StringComparison.OrdinalIgnoreCase)) { // If a base virtual path is specified, we should remove it as a prefix. Everything that follows should map to a view file on disk. if (!String.IsNullOrEmpty(baseVirtualPath)) { virtualPath = "~/" + virtualPath.Substring(baseVirtualPath.Length); } string path = HostingEnvironment.MapPath(virtualPath); return File.Exists(path) && File.GetLastWriteTimeUtc(path) > assemblyLastWriteTime.Value; } return false; }
And when looking at the detailed trace data the client had captured, the server in question was spending a considerable amount of time in the
File.Exists
and
File.GetLastWriteTimeUtc
methods that this calls. No individual call took much time, but there were a lot of them.
Looking at the views in the client's site, it was clear that they were not pre-compiling their views, and that their pages were broken up into a large number of nested view files that were largely included as partials. I wanted to try and determine if this issue was specific to the client's code or whether it was a more general issue, so I spent some time attempting to recreate the issue in a blank instance of Sitecore 8.2 with just a couple of view files added.
The simplest model I could come up with was to have a View Rendering which called
HTML.Partial()
in a loop, to simulate a complex set of nested partial views. With this as a parent view:
<h2>Using Partials</h2> @for (int i = 0; i < 100; i++) { @Html.Partial("~/views/ViewPerformanceTest/SimpleSubView.cshtml") }
and this as the child partial view:
<div>View: @DateTime.Now.Ticks</div>
In order to verify that the issue was not specific to binding the child views as partials, I also tried a variation of the parent view that bound the children with Sitecore's
ViewRendering()
method:
@using Sitecore.Mvc <h2>Using ViewRenderings</h2> @for (int i = 0; i < 100; i++) { @Html.Sitecore().ViewRendering("/views/ViewPerformanceTest/SimpleSubView.cshtml") }
And with these deployed to a copy of the appropriate version of Sitecore, I tried to see if I could replicate the client's results. Which I could:
I tried a variety of tests, with compiled views present and not present, with
UsePhysicalViewsIfNewer
enabled and disabled, with view files present and not present, and using both Partial and ViewRendering bindings. Under v8.2 all of these were showing similar behaviour where > 40% of the time for each request was spent in this bit of RazorGenerator. As a comparison I also tried running the same views under a copy of V8.1 – where (unsurprisingly) these overheads were not visible.
Overall, what I've learned here is to remember that the startup-time improvements that RazorGenerator brings to v8.2 can come with a trade-off. You get rid of the compilation time overhead that you would have seen on startup in v8.1, but you get a small chunk of time spent testing the state of the
cshtml
file each time you reference a view.
From the perspective of the RazorGenerator project (which is aimed at "normal" MVC) this probably isn't a big thing. Most MVC pages don't include that many views, so the overhead isn't a major issue. But given that Sitecore pages can include many controllers and views for the individual UI components, and these can in turn include further child views, the small overhead per view can mount up. And it seems some projects with complex pages can add sufficient overhead for it to get noticeable.
So what can we do about this? Probably two key things:
Kamruz Jaman commented:I think the article you are referring to is probably this: https://chrisperks.co/2018/03/22/hundreds-of-renderings-your-first-page-load-could-be-sloooow/
I've used RazorGenerator.MVC on several projects and have never noticed any issues (but we use Sitecore caching, so it's entirely possible that we just did not notice). The above article uses a different pre-compiler so wonder if the results would be different? One advantages of using a precompiler is compile-time errors being thrown instead of run-time errors (for example, if you deleted a property in a model that was still referenced in a view), so there are other benefits IMO (you can set
MvcBuildViews=true
but this massively increased build times for us).I wonder what your overall execution time would be using Pre-compiled vs non-compiled views would be in **your project**, i.e. add the
RazorGenerator.MVC
nuget package to your solution, would the overall time to compiling your views less than the impact of the check. You should also try clearing out the Temporary ASP.NET Files folder in case that was causing issues with you being able to disable “UsePhysicalViewsIfNewer”. Just some thoughts anyway….
Editied to add:↑ Back to topTwoThree things:Firstly, thank you Kamruz, the post that I was referring to was indeed this one.
Secondly I got asked if I was saying "don't use compiled views" – to which the answer is a resounding no. The points made by Chris in the post that jogged my memory (and Kam in his article on the topic) are perfectly valid. I'm discussing a specific edge case that you may see with complex sites that don't output cache much.
And more recently: In old comments, Ian points out that Sitecore Support have a patch for v9 that may help resolve problems related to this. If you're having issues, that may help...