Precompile Your Sitecore Content Delivery Site

I wanted to share my experience precompiling views in my Sitecore 9.1 website in case someone else out there wants to try it. Spoiler alert - it's worth the effort.

I'm working on a site where almost every page is composed of dozens of dynamic MVC components, which made the pages unacceptably slow to load the first time you visit the page. This is because every view would be compiled at runtime on the first request for the view. In an effort to reduce this startup time I decided to try to pre-compile the entire website. Precompiling your site has several advantages including dramatically reducing page startup times, and improving code quality (because your views are checked for errors at compile time), and preventing rogue developers from pushing .cshtml changes to your production site. 

On the downside it takes a bit longer to build and deploy the solution, but that was not a deal-breaker for me.

I decided to pre-compile my Content Delivery website and NOT my Content Management site, because pre-compiling the CM comes with it's own set of challenges (especially with SPEAK views), and my main concern was CD performance. I also decided that my local development site would NOT be precompiled to speed development. Any view compilation errors would be detected during continuous integration builds. 

My site has the following characteristics that are relevant to my precompilation effort:

  • Helix solution architecture
  • Coveo Hive site search and search-driven components
  • Glass Mapper
  • Azure pipelines to build and deploy

First, I did the easy part - updating my Azure pipeline to precompile the site. This was accomplished by adding a couple more MSBuild arguments to my CD build pipeline. Both of these params are required:

PrecompileBeforePublish=true  --  Directs MSBuild to precompile all the views found in the solution.
EnableUpdateable=false  --  Directs MSBuild to include the view's markup in the compiled assembly, thus preventing updates to the view after compilation.

My final MSBuild arguments look like this:

/p:PrecompileBeforePublish=true /p:EnableUpdateable=false /p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:Configuration=ProdCD1 /p:PackageLocation="$(build.artifactstagingdirectory)\\" /verbosity:normal

I ran the pipeline and inspected the output files on the published website. As expected, there were a whole bunch of new assemblies in the /bin folder, and contents of all the .cshtml only had the text "This is a marker file generated by the precompilation tool, and should not be deleted!". Great! All the view code has been compiled and moved into the assemblies.

Next, I tried to view the precompiled site in a browser and ran into my first roadblock:

Server Error in '/' Application.

  The model item passed into the dictionary is of type 'Sitecore.Mvc.Presentation.RenderingModel', 
but this dictionary requires a model item of type 'xxxxx.Models.xxxViewModel'

 

My gut told me this was some kind of Glass Mapper problem, and on further investigation my assumption was proven correct. It seemed like Glass could not determine the ViewRendering type based on the view file content - which makes sense because there's nothing in the view's .cshtml file anymore that Glass could use to determine type. 

After much digging through the Glass source code, I found the solution which consisted of adding a new class to my Foundation Glass module, like so:

using Glass.Mapper.Sc.Pipelines.Response;
using System;
using System.Web.Mvc;
using Sitecore.Mvc.Common;
using System.Reflection;
using SC = Sitecore;
using System.Web.Compilation;

namespace xxxxx.Foundation.Orm.App_Start
{
    public class CompiledViewTypeFinder : IViewTypeResolver
    {
        public Type GetType(string path)
        {
            try
            {
                Type compiledViewType = BuildManager.GetCompiledType(path);
                Type baseType = compiledViewType.BaseType;
                if (baseType == null || !baseType.IsGenericType)
                {
                    Sitecore.Diagnostics.Log.Warn(string.Format(
                        "View {0} compiled type {1} base type {2} does not have a single generic argument.",
                        path,
                        compiledViewType,
                        baseType), this);

                    return typeof(NullModel);
                }
                Type proposedType = baseType.GetGenericArguments()[0];
                return proposedType == typeof(object)
                    ? typeof(NullModel)
                    : proposedType;
            }
            catch (Exception ex)
            {
                SC.Diagnostics.Log.Error("CompiledViewTypeFinder Path = " + path, ex, this);
                return null;
            }
        }
    }
}

And then calling this method in the PostLoad() method of my GlassMapperScCustom.cs (which lives in /App_Start), like so:

public static void PostLoad(){
    // DFR|8/1/2019: Support for precompiled views.
    GetModelFromView.ViewTypeResolver = new ChainedViewTypeResolver(
    new IViewTypeResolver[] {
    new CompiledViewTypeFinder(),
    new RegexViewTypeResolver() });
}

So time to try again! I committed my code updates, ran the build again and brought up the site. This time I got this error:

Server Error in '/' Application.

  The file '/Views/Coveo Hive/Search Interfaces/Coveo Search Interface.cshtml' has not been pre-compiled,
and cannot be requested.

 

One look at this and it was clear I needed to add ALL my Coveo Hive views to the solution (not just my custom ones) so that they, too, could be compiled. The moral of this story is when precompiling views it's all or nothing.

I also ran into this error, which would cause Experience Analytics to stop working if not fixed:

Server Error in '/' Application.

  The file '/layouts/System/VisitorIdentification.aspx' has not been pre-compiled,
and cannot be requested.

 

My approach to this one was to remove @Html.Sitecore().VisitorIdentification() from my main layout, and replace it with the actual rendered code for visitor identification:

<meta name="VIcurrentDateTime" content="@DateTime.UtcNow.Ticks" />
<script type="text/javascript" src="/layouts/system/VisitorIdentification.js"></script>

Once that was done I ran the pipeline a final time and VOILA! The site came up faster than ever, and I was living in precompiled heaven. I'm sold - every project I do from now on will have a precompiled CD site.

I'm sure there are other, and perhaps even better ways to do this. I'd love to hear how YOU pulled it off in the comments section.

Happy precompiling,
David

Add comment

Loading