Friday, February 19, 2021

Sitecore JSS cache and performance

 https://hishaamn.wordpress.com/2019/09/25/sitecore-jss-integrated-mode-cache/


https://hishaamn.wordpress.com/2020/06/04/sitecore-my-cache-helper/


ollowing a very hectic week, I have been experiencing performance issues on my JSS App. The first byte of my site was around 2 to 3 seconds which is not acceptable. I’ve executed a performance profiler from Visual Studio and identified some expensive Sitecore queries. Even though the queries have been optimized, the first byte was still high. Then, I’ve checked the Sitecore Component Caching. I could see the custom cache that I have implemented increasing. So WHY is the first byte still high?

I had to start by looking at the response of my Layout Service just in case there is any connectivity issue. Then, I did a showconfig to see the different pipelines that are responsible for the rendering of my JSS App. After some intensive debugging, I found out that it is the conversion of the JSON response to HTML being the culprit. The reason is because the Homepage has lots of components. So, the JSON being returned is big. It is the time taken to convert the JSON to HTML that caused my first byte to peek.

Here is a diagram which explain how the process works

Untitled-1.png

So, when a user request a page, the layout service will process the different JSS Components and then pushed them into the Sitecore cache so that next time a same component is requested, it reads from the cache. It caches only the JSON, not the HTML. The JSON is then converted to HTML which is then served to the end user. This is where the processing takes time. The larger your JSON response, the larger the processing time.

Solution was to cache the HTML. The main drawback of caching the HTML itself is that you can no more have dynamic contents to be pushed to the site. If you still want to cache the full HTML, continue to read. More information is available on the YouTube Video performed by Anastasiya Flynn

The first thing to do is to extend theGetJsLayoutRenderer. This is because in theGetJsLayoutRenderer, it is instantiating a new object of typeJsLayoutRenderer.

using Sitecore.Abstractions;
using Sitecore.JavaScriptServices.Configuration;
using Sitecore.JavaScriptServices.ViewEngine.Presentation;
using Sitecore.JavaScriptServices.ViewEngine.Presentation.Pipelines.MvcGetRenderer;
using Sitecore.LayoutService.Configuration;
using Sitecore.LayoutService.ItemRendering;
using Sitecore.LayoutService.Serialization;
using Sitecore.Mvc.Pipelines.Response.GetRenderer;
namespace Experimental.Foundation.Pipelines.Renderer
{
public class GetJsLayoutRendererExtension : GetJsLayoutRenderer
{
public GetJsLayoutRendererExtension(ILayoutService layoutService, ISerializerService serializerService, IConfiguration layoutServiceConfiguration, IConfigurationResolver appConfigurationResolver, IJssRendererConfiguration jssRendererConfiguration, BaseCorePipelineManager pipelineManager) : base(layoutService, serializerService, layoutServiceConfiguration, appConfigurationResolver, jssRendererConfiguration, pipelineManager)
{
}
protected override Sitecore.Mvc.Presentation.Renderer GetRenderer(GetRendererArgs args)
{
AppConfiguration appConfig = this.ResolveAppConfiguration(args.Rendering.Item);
if (appConfig == null)
{
return new JssAppNotFoundStandardValuesRenderer();
}
NamedConfiguration layoutServiceNamedConfig = this.ResolveNamedConfiguration(appConfig);
return new JsLayoutRendererExtension(args.Rendering, appConfig, layoutServiceNamedConfig, this.LayoutService, this.SerializerService, this.JssRendererConfig);
}
}
}

Once the above have been implemented, you need to extend theJsLayoutRendererwhich I named mine asJsLayoutRendererExtension. Below is the implementation of the custom renderer.

using System.IO;
using Experimental.Foundation.Utilities.Helpers;
using Newtonsoft.Json;
using Sitecore.Data.Items;
using Sitecore.JavaScriptServices.Configuration;
using Sitecore.JavaScriptServices.ViewEngine.Presentation;
using Sitecore.JavaScriptServices.ViewEngine.RenderingEngine;
using Sitecore.LayoutService.Configuration;
using Sitecore.LayoutService.ItemRendering;
using Sitecore.LayoutService.Serialization;
using Sitecore.Mvc.Presentation;
namespace Experimental.Foundation.Pipelines.Renderer
{
public class JsLayoutRendererExtension : JsLayoutRenderer
{
public JsLayoutRendererExtension(Rendering rendering, AppConfiguration appConfig, NamedConfiguration layoutServiceNamedConfig, ILayoutService layoutService, ISerializerService serializerService, IJssRendererConfiguration jssRendererConfiguration) : base(rendering, appConfig, layoutServiceNamedConfig, layoutService, serializerService, jssRendererConfiguration)
{
}
protected override object[] ResolveFunctionArgs()
{
string requestPath = GetRequestPath();
Item itemToRender = GetItemToRender();
RenderedItem rendered = RenderItem(itemToRender, LayoutServiceNamedConfiguration);
dynamic viewBag = GetViewBag(GetViewBagPipelineArgs(itemToRender));
string text = SerializerService.Serialize(rendered, LayoutServiceNamedConfiguration.SerializationConfiguration);
object obj = JsonConvert.SerializeObject(viewBag, LayoutServiceNamedConfiguration.SerializationConfiguration.JsonSerializerSettings);
var cacheKey = $"{rendered.ItemId}-{rendered.ItemLanguage}";
return new []
{
requestPath,
text,
obj,
cacheKey
};
}
protected override void PerformRender(TextWriter writer, IRenderEngine renderEngine, string moduleName, string functionName, object[] functionArgs)
{
var cacheKey = functionArgs[3] as string;
// Apply/Read cache here
var renderResult = CacheHelper<RenderResult>.GetOrCreateCache(cacheKey,
() => renderEngine.Invoke<RenderResult>(moduleName, functionName, functionArgs));
if (!string.IsNullOrWhiteSpace(renderResult.Redirect))
{
if (!renderResult.Status.HasValue || renderResult.Status == 302)
{
JssRendererConfiguration.HttpContext.Response.Redirect(renderResult.Redirect);
}
else
{
JssRendererConfiguration.HttpContext.Response.RedirectPermanent(renderResult.Redirect);
}
}
if (renderResult.Status.HasValue)
{
JssRendererConfiguration.HttpContext.Response.StatusCode = renderResult.Status.Value;
}
writer.Write(renderResult.Html);
}
}
}

From the code snippet above (JsLayoutRendererExtension.cs), the first change I’ve made is to add a 4th entry into the array which is the cache key. This is because the rendered.ItemId provides me with the current item id and the caching should be applied for each page.

Second is the renderEngine.Invoke<RenderResult>(moduleNamefunctionNamefunctionArgs) which is responsible to converts the JSON to HTML. Note that I have implemented a custom Sitecore cache where the implementation is found in the CacheHelper method. You can use your own cache implementation but leave a comment if you want me to share the CacheHelper code.

Once all the codes have been implemented, you need to apply a config patch to use the custom renderer than the currentGetJsLayoutRenderer. Below is the patch I am using.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/&quot; xmlns:role="http://www.sitecore.net/xmlconfig/role/&quot; xmlns:search="http://www.sitecore.net/xmlconfig/search/"&gt;
<sitecore>
<pipelines>
<mvc.getRenderer>
<processor type="Experimental.Foundation.Pipelines.Renderer.GetJsLayoutRendererExtension, Experimental.Foundation.Pipelines" patch:instead="*[@type='Sitecore.JavaScriptServices.ViewEngine.Presentation.Pipelines.MvcGetRenderer.GetJsLayoutRenderer, Sitecore.JavaScriptServices.ViewEngine']" resolve="true">
<layoutServiceConfigurationName>jss</layoutServiceConfigurationName>
</processor>
</mvc.getRenderer>
</pipelines>
</sitecore>
</configuration>

No comments:

Post a Comment