Reinoud van Dalen

January 31, 2017

Sitecore placeholders and xEditor performance

A quick blog on something I discovered recently. If you have a page with a lot of renderings and many placeholders, then the xEditor’s performance can drastically get affected. In my efforts to fix this behavior for one of our clients I found a solution I’d like to share.

15 seconds to load

What we noticed was that some pages with a lot of renderings were having performance issues. Like 15 seconds to load and every time we’d make changes to the layout we would have to wait another 15 seconds. Other pages with less renderings did not have these symptoms, but 15s was definitely unbearable.

Atomic design killed the page

It’s worth mentioning that this Sitecore solution took Atomic Design to the extreme and pages could have up to 250+ renderings and a huge amount of (dynamic) placeholders.

Now I’m not going to debate if that’s a good thing, I was just tasked to find out why these pages were slow. But obviously I thought having that many was a quick recipe for disaster as anything that takes a bit of time would take a lot of time if you multiply it with 250.

GetLayoutDetails processor

Having dotTrace fire up a profiling session quickly showed me that a lot of time was spent in the GetLayoutDetails processor. This processor is responsible for decorating the ChromeData for placeholders with stuff like DisplayName, allowedRenderings and if it’s editable or not.

But specifically resolving the allowedRenderings was causing the issue. The thing is that it needs the layout xml of the current page to determine the allowed renderings. Getting the layout xml requires resolving layout source fields, patching the layout and apply delta’s – all done in the LayoutField.GetFieldValue. But the processor redoes this for every placeholder. So imagine patching a layout containing 250+ renderings and hundreds of placeholders and doing it for every placeholder.

Cache the layout

So then I thought: why do we need to resolve the layout again and again? In theory we’d only need to do it once per request. Putting it to the test wasn’t that hard. I ended up with a modified processor and replaced the original in a patch config. I've added a comment on where I modified the original and added a GetCachedLayout method which tries to get the layout from the request before resolving it via the usual way.

public class CustomGetPlaceholderChromeData : GetChromeDataProcessor
{
    ///The chrome type.
    public const string ChromeType = "placeholder";
    ///The key of the placeholderkey in CustomData dictionary.
    public const string PlaceholderKey = "placeHolderKey";
    ///Path to the root item default buttons.
    private const string DefaultButtonsRoot = "/sitecore/content/Applications/WebEdit/Default Placeholder Buttons";

    ///The process.
    ///The pipeline args.
    public override void Process(GetChromeDataArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
        Assert.IsNotNull(args.ChromeData, "Chrome Data");
        if (!ChromeType.Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase)) return;

        var placeholderPath = args.CustomData[PlaceholderKey] as string;
        Assert.ArgumentNotNull(placeholderPath, "CustomData[\"{0}\"]".FormatWith(PlaceholderKey));

        var placeholderKey = StringUtil.GetLastPart(placeholderPath, '/', placeholderPath);

        args.ChromeData.DisplayName = placeholderKey;
        AddButtonsToChromeData(GetButtons(DefaultButtonsRoot), args);

        Item placeholderItem = null;
        var hasPlaceholderSettings = false;
        if (args.Item != null)
        {
            //this is the part I modified:
            var layout = GetCachedLayout(args.Item);

            var placeholderRenderingsArgs = new GetPlaceholderRenderingsArgs(placeholderPath, layout, args.Item.Database)
            {
                OmitNonEditableRenderings = true
            };
            CorePipeline.Run("getPlaceholderRenderings", placeholderRenderingsArgs);

            hasPlaceholderSettings = placeholderRenderingsArgs.HasPlaceholderSettings;
            var stringList = placeholderRenderingsArgs.PlaceholderRenderings?.Select(i => i.ID.ToShortID().ToString()).ToList() ?? new List();
            args.ChromeData.Custom.Add("allowedRenderings", stringList);
            placeholderItem = Client.Page.GetPlaceholderItem(placeholderPath, args.Item.Database, layout);
            if (placeholderItem != null)
                args.ChromeData.DisplayName = HttpUtility.HtmlEncode(placeholderItem.DisplayName);
            if (!string.IsNullOrEmpty(placeholderItem?.Appearance.ShortDescription))
                args.ChromeData.ExpandedDisplayName = HttpUtility.HtmlEncode(placeholderItem.Appearance.ShortDescription);
        }
        else
            args.ChromeData.Custom.Add("allowedRenderings", new List());
        var isEditable = (placeholderItem == null || placeholderItem["Editable"] == "1") && (Settings.WebEdit.PlaceholdersEditableWithoutSettings || hasPlaceholderSettings);
        args.ChromeData.Custom.Add("editable", isEditable.ToString().ToLowerInvariant());
    }
    
    //Instead of calling ChromeContext.GetLayout(layoutItem) every time, we store it in the request so we only need to do it once
    protected virtual string GetCachedLayout(Item layoutItem)
    {
        var layoutCacheKey = $"edit-layout-item-{layoutItem.ID}";
        var layout = Context.Items[layoutCacheKey] as string ?? ChromeContext.GetLayout(layoutItem);
        Context.Item[layoutCacheKey] = layout;

        return layout;
    }
}

The patch config then looks like:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getChromeData>
        <processor type="(Namespace).CustomGetPlaceholderChromeData, (AssemblyName)" patch:instead="processor[@type='Sitecore.Pipelines.GetChromeData.GetPlaceholderChromeData, Sitecore.Kernel']"/>
      </getChromeData>
    </pipelines>
  </sitecore>
</configuration>

Gained 9 seconds!

The results were great. The 15 seconds were cut down to 6. I actually needed to modify the Dynamic Placeholder processor as well because it needed the same layout xml as well.

The remaining 6 were further cut down to 2,5 because a custom dataprovider was getting called more than it should.

Conclusion

Now I don’t see a lot of Sitecore solutions that have that many renderings and placeholders that often. So there’s a good chance that the impact is too low to notice for most of you. But knowing Dynamic Placeholders and Atomic Design being a thing it might be an issue in the making.

I checked in with Sitecore support and they marked caching the layout as a legit solution. They even filed it as Wish/Feature Request.

I’m hoping this will help someone else and would like to hear it if it does.

TAGS: sitecore xEditor performance


Comments
Jan Bluemink

Nice, great idea caching the layout. Thanks for sharing.


David San Filippo

This worked really well for me. Went from 15 seconds to under 3 seconds for Experience Editor Loads. The code sample was a little messed up though. Besides character formatting issues, the last if statement before the main else: "!string.IsNullOrEmpty(placeholderItem?.Appearance.ShortDescription)) needed to be changed to check that placeholderItem is not null too: "if (placeholderItem != null && !string.IsNullOrEmpty(placeholderItem.Appearance.ShortDescription)) Thanks for this.


Adamsimsy

Nice little solution. I do hope they get this into Sitecore soon. I've experienced rich/complex Sitecore experience editor views taking a long time to render. Keeping component configuration defeats the point of the flexibility of the editor so I hope they release this as KB patch. Out of interest, what did you build your blog with, Jekyll?


Reinoud van Dalen

Hi Dan, great to hear you managed to fix the performance! The mess-up are the null-conditional operators that come with C# 6, basically the same result as what you made of it.


Reinoud van Dalen

Hi Adamsimsy, I built this blog on Umbraco, using a free bootstrap template. It's a bit simple and outdated but it works :)


Reinoud van Dalen

Hi Bruno, there could be several reasons you don't see the GetLayoutDetails as bottleneck. I think the most probable one, as explained in the conclusion, is that most solutions dont have enough in the layout to notice the difference. I'll update the post with the patch configuration.


Bruno

Hi Reinoud. I'm trying to follow your steps, I fired up DotTrace but I don't see the GetLayoutDetails on the timeline as the one that takes a long time. Do you mind explaining in more detail how you got to that conclusion? And would mind posting as well the patch file? Thanks a lot. Bruno