Hitting the Nitro—Caching in Kentico Xperience 13 Refresh 1

Hitting the Nitro—Caching in Kentico Xperience 13 Refresh 1

Mar 25, 2021
Kentico MVP & Senior Software Engineer at Heartland Business Systems
Hitting the Nitro—Caching in Kentico Xperience 13 Refresh 1

Kentico Xperience 13 in .Net Core is fast, way faster than MVC, and tremendously faster than webform portal engine. In a recent blog article, I highlighted how fast Kentico Xperience 13 was in .Net Core compared to Kentico Xperience 12 MVC using the same Baseline site. Even without caching, Kentico Xperience 13 Core blew version 12 out of the waters. It showed average render speeds in the 10–30 millisecond range (this after the data was cached), where Kentico Xperience 12 often had larger first-hit loads and only beat these speeds with output caching. In Refresh 1, Kentico Xperience introduces integration with an existing Core feature (Cache Tag), an addition to the vary-by options, and something completely new—Page Builder Caching. Let us dive into each and see how they work and how fast they make things!

<Cache> Tag Helper and <cache-dependency>

.Net Core MVC features a <cache></cache> tag helper, which caches all the content within it, resulting in a reduction of queries and view rendering into a simple memory lookup and display. This tool was difficult to use before the Refresh because there was no way to tell the cache when it should clear early. Kentico Xperience has a great Cache Dependency Key system that triggers when updates are made in the CMS to automatically clear its data caches when appropriate. The Refresh brings a <cache-dependency cache-keys="@string[]{"Keyhere"}" /> tag helper that you can nest within the <cache/>  tag in order to set these dependencies. You can cache a large area for a long period of time, while also having it refresh as soon as some inner content is modified.

How you obtain those Cache-Dependencies is another matter, as ideally, you want all the various cache dependencies for all the data items called within it to be added. Luckily (with some help from Sean G. Wright), we have you covered!

MVC Caching Integration

For those of you using the MVCCaching tool I created, version 13.0.2 now comes baked in with a new ICacheDependenciesScope/ICacheDependenciesStore interfaces, which allow you to ICacheDependenciesScope.Begin() and ICacheDependenciesScope.End()  your cached area, ICacheDependenciesStore.Store(string[] Dependencies) -ing all your dependencies along the way and returning them in the ICacheDependenciesScope.End() method (it returns a string array). Any method called from an IRepository that uses the [CacheDependency(““)] attributes will automatically have these dependencies added to the Store, and of course you can add your own manually.

Here are some code samples:

_ViewImports.cshtml:
@using MVCCaching.Base.Core.Interfaces

@using MVCCaching.Interceptor;

@addTagHelper *, Kentico.Web.Mvc

@inject ICacheDependenciesScope CacheScope

@inject ICacheDependenciesStore CacheStore

@inject ICachingRepositoryContext CacheContext

 

_Layout.cshtml:

<header>

        <cache expires-after="@TimeSpan.FromMinutes(60)" enabled="@CacheContext.CacheEnabled()">

            @{CacheScope.Begin();}

            @* Example of manually adding *@

            @{CacheStore.Store(new string[] { $"documentid|{PWPHelper.GetDocumentIDByNode("/Masterpage/Header")}" });}

            <inlinewidgetpage documentid="@PWPHelper.GetDocumentIDByNode("/Masterpage/Header")" initialize-document-prior="true">

                <vc:partial-header />

            </inlinewidgetpage>

            <cache-dependency cache-keys="@CacheScope.End()" />

        </cache>

 

        @* Cache is on the component itself *@

        <cache expires-after="@TimeSpan.FromMinutes(60)" enabled="@CacheContext.CacheEnabled()">

            @{CacheScope.Begin();}

                @* This View Component uses various IRepository calls that are hooked up with MVCCaching, so all dependencies are added to the Store automatically *@

                <vc:main-navigation navigation-parent-path="/MasterPage/Navigation" css-class="MainNav" />

            <cache-dependency cache-keys="@CacheScope.End()" />

        </cache>

        @* This adds javascript to highlight the current nav, this way we don't need a Vary-By on the menu and it can be cached for all pages *@

        <navigation-page-selector parent-class="MainNav" />

    </header>

As you can see here, we have two header items cached. One is my Partial Widget Page that pulls in the widget zone content from a separate header page, so it has a cache dependency of that DocumentID.

The Second is the main navigation, which uses IRepository method calls that are part of my MVCCaching system.  I simply Begin(), -Render-, and End(), passing the Dependencies collected to the <cache-dependency cache-keys="@CacheScope.End()" />.

You can see how the CacheDependencyStore is implemented, as well as how it ties into the MVCCaching system.

Other Integrations

The ICacheDependenciesScope and ICacheDependenciesStore can be recreated easily if you do not use MVCCaching, Sean’s awesome system he outlines in his repo, just add the Store right at this point in his QueryHandlerCacheDecorator. He also provided a brief outline of his original pattern idea in this GIST. I would only adjust it as I have to have End() return a string array, so you don’t need to do a “.ToArray()” each time you want to pass the dependencies to the cache-keys.

New Vary-By

Vary-By allows you to vary the caches by some “thing.” This is great if your cache depends on some outside variable and should render differently based on that. Here are the default Vary-Bys:

  • Vary-by-header: Allows you to vary by a header parameter on the request (ex: “User-Agent” or “User-Agent,content-encoding”
  • vary-by-query: Allows you to vary by some URL query parameter (ex: “category” for ?category=someval)
  • vary-by-route: Vary by route-values determined by your route Mapping (ex “Make” would vary in a route of {controller}/{action}/{Make} 
  • vary-by-cookie: Varies by cookie values (ex "CookieLevel")
  • vary-by-user: true or false, varies by the User.Identity.Name
  • vary-by: varies by the passed parameters. ToString(), ex ( @Model.SomeVal1+@Model.SomeVal2)

Refresh 1 introduces these new vary-by parameters:

  • vary-by-cookie-level: Ties into Xperience’s Cookie Level permissions.
  • vary-by-culture: true/false, if the rendering should change based on the user’s culture
  • vary-by-host: true/false, if the content should differ based on the Domain name it is being rendered on (useful for multi-site instances sharing the same MVC application)
  • custom vary-by: Refresh allows you to create your own Vary-by-option through the ICacheVeryByOption interface and the [CacheVeryBy] attribute on the cached ViewComponent


Using Vary-By and Performance implications

You should use Vary-by parameters whenever indeed the content would change based on any of the above rules. However, keep in mind that you will be creating multiple copies of the object in memory by doing this. If the cached area takes up a large amount of memory, you could end up with a larger memory imprint that would offset the benefit.  And if the Vary-by is often different, you could end up with items rarely using their matching cache, nullifying the benefits of caching.

For example, in my original baseline site, the Main navigation would pass the current page to the Menu so it could add an “Active” class onto the page that the user is on. This means that to do this, I would need a vary-by=” {TheCurrentPage}” so each page would render a new menu with the proper item selected. It resulted in every first visit to each page generating a new menu, and if I had many pages, the benefit would be lessened, and the memory imprint would increase.

So, what I did was I modified the menu so that the logic to set the “Active” class was done client-side in a separate non-cached code block. Thanks to it, the entire menu could be cached without a vary-by. Each page then used the cached menu, and a quick piece of JavaScript would instantly set the active class. One cached item shared on all pages.

Widget and Page Builder Caching

The final thing Refresh 1 brings is caching to Page Builder Widgets! It is a 2-part system, where on the page builder zones (<editable-area/>), you have to allow caching (allow-widget-output-cache=” true”) and the cache-duration (widget-output-cache-expires), and then any widgets within it that also enable caching (through the AllowCache = true parameter on the RegisterWidget assembly tag).

Like this:

<editable-area area-identifier="main" widget-output-cache-expires-after="@TimeSpan.FromMinutes(60)" allow-widget-output-cache="true"/>

 

[assembly: RegisterWidget(ShareableContentWidgetViewComponent.IDENTITY, typeof(ShareableContentWidgetViewComponent), "Shareable Content"typeof(ShareableContentWidgetProperties), Description = "Displays the widget content of a Shareable Content Page", IconClass = "icon-recaptcha",AllowCache = true)]

Then on the ViewComponentModel property passed to your View Component, there will be a CacheDependencies or CacheKeys property that you can set to include these dependencies. Don’t forget you can use the [CacheVaryBy()] Attribute on these to ensure the caching changes if it varies by something externally.

Result: How much faster?

As stated, I tested these features on a clone of the baseline, so I had two sites running that were identical except for these features enabled. I then created a simple console script to hit the pages X number of times and average their response times in milliseconds (I did 100 hits). The script warmed up the page with five un-tracked requests to ensure caching was set, and data-caching was working on both, so we are only testing the performance increase of the <cache> tag and the page builder caching.

Feature

KX12 MVC (Data caching only)

KX12 MVC (Output Cache)

Pre Refresh 1: (Data caching only)

Refresh 1: Data and Render Caching

Performance Increase (over KX 13 Pre)

Home Page (empty)

20.15 ms

5.3 ms

18.84 ms

6.85 ms

65% Faster

About Page (Some content)

20.71 ms

N/A

 

24.32 ms

14.53 ms

40% Faster

Sub Page (Secondary Side Navigation)

16.96 ms

5.16 ms

25.71 ms

16.39 ms

36% Faster

Inline Widget Page (3 Widgets) – Page Builder Cache Disabled

N/A

 

N/A

 

24.66 ms

20.20 ms

18% Faster over Base

Inline Widget Page (3 Widgets) – Page Builder Cache Enabled

N/A

 

N/A

 

24.66 ms

13.25 ms

35% Faster than Non-cached Refresh 1

 

The performance increases before and after Refresh 1 are obviously huge improvements to an already fast rendering.  

And compared to Kentico Xperience 12 MVC’s Output Cache, it should be noted that nothing really ‘beats’ output caching speed because it literally just spits out the entire output once cached. However, I would not let that make you think that .Net Core can’t perform as great.  

Output caching in Kentico Xperience 12 cached everything, and if anything had a variation, then everything had to be re-rendered for that unique output cache. .Net Core achieves comparable performance, but each element is cached individually, meaning if something changes in just the menu, only the menu is re-done, and that is shared. So overall, .Net Core with this caching is giving better page performance. MVC non-cached pages continue to have much greater performance hits than .Net Core.

Conclusion

Cache dependency and Page Builder Caching were a considerable performance increase, and that alone is enough reason to get your solutions updated! The HBS Baseline will be updated sometime after the Refresh launches if you are new to Kentico Xperience 13 and want a starting point, as well as if you use the Baseline and wish to copy some of these performance increases.

Be sure to leverage these new features as you build out your widgets and solutions so that your sites stay fast and responsive—and your customers happy!


We're sorry, but your browser is currently not supported. Try using our website in other browsers like the new Microsoft Edge, Google Chrome, or Mozilla Firefox.
Should you have any query or want to report any issue, feel free to send us an email to support@kentico.com.