JSS: Reducing Bloat in Multilist Field Serialization

> Because: performance, security, and error-avoidance
Cover Image for JSS: Reducing Bloat in Multilist Field Serialization

The Problem

I was recently involved in a Slack conversation around the topic of reducing the size of the serialized payload in JSS. The conversation was prompted by a Sitecore Support ticket that was opened by a customer. The customer was experiencing issues with the Experience Editor failing to load when the serialized payload was too large.

The problem can be summarized as follows:

Let's say you have an item with a list field that references multiple items, and the items you are referencing have many fields. The default JSS behavior is to serialize all of those items' fields. You may not want this to be the case, either for performance reasons or because you don't want to expose that information to the client.

Experience Editor Issues

Have you ever seen the JS build logs warn you about serializes pages being too large? Take those warnings seriously; you may want to do some optimization.

If the serialized payload is too large (i.e. larger than 2MB), it can cause the Experience Editor to fail during load. Keep in mind that Experience Editor adds its own bloat to the payload in the form of editable chrome markups.

Sitecore Support recommended the following:

You can mark certain unnecessary fields as non-editable to trim down the serialized payload. To achieve this, you can create custom serializers for such field types extending from the BaseFieldSerializer class. Then you can override the RenderField method and set disableEditing to true for the unnecessary fields based on a condition (for example, field name or field id). The serializers will then need to be added to or replaced in the getFieldSerializer pipeline (under layoutService group) in Sitecore configuration.

One Approach

I propose a different approach that is arguably more powerful. We can override the GetMultilistFieldSerializer with our own. Methods can then be overridden with logic just for the list fields. If the field isn't a list type, we can call the base.

This method allows us to only serialize the fields that we want. By following this guide, you should also be in a good place to "override the RenderField method and set disableEditing to true for the unnecessary fields".

Note that the examples below use dependency injection, which is a pattern I highly recommend for Sitecore solutions. The examples are intentionally incomplete to avoid cluttering the post with the usual minutia of dependency injection configuration.

Specify the Custom GetMultilistFieldSerializer

<pipelines>
<group groupName="layoutService">
<pipelines>
<getFieldSerializer performanceCritical="true">
<processor type="Sitecore.LayoutService.Serialization.Pipelines.GetFieldSerializer.GetMultilistFieldSerializer, Sitecore.LayoutService" resolve="true">
<patch:attribute name="type">Website.Foundation.LayoutService.Serialization.Pipelines.GetFieldSerializer.GetMultilistFieldSerializer, Website</patch:attribute>
</processor>
</getFieldSerializer>
</pipelines>
</group>
</pipelines>
using Sitecore.Abstractions;
using Sitecore.Diagnostics;
using Sitecore.LayoutService.Serialization;
using Sitecore.LayoutService.Serialization.Pipelines.GetFieldSerializer;
using Sitecore.Services.GraphQL.EdgeSchema.Services.Multisite;
using Website.Foundation.LayoutService.Serialization.FieldSerializers;
using Website.Foundation.LayoutService.Services;
namespace Website.Foundation.LayoutService.Serialization.Pipelines.GetFieldSerializer
{
public class GetMultilistFieldSerializer : Sitecore.LayoutService.Serialization.Pipelines.GetFieldSerializer.GetMultilistFieldSerializer
{
private readonly BaseMediaManager _mediaManager;
private readonly IMultisiteService _multisiteService;
private readonly ISerializedTargetItemService _serializedTargetItemService;
public GetMultilistFieldSerializer(
IFieldRenderer fieldRenderer,
BaseMediaManager mediaManager,
IMultisiteService multisiteService,
ISerializedTargetItemService serializedTargetItemService) : base(fieldRenderer, mediaManager, multisiteService)
{
_mediaManager = mediaManager;
_multisiteService = multisiteService;
_serializedTargetItemService = serializedTargetItemService;
}
protected override void SetResult(GetFieldSerializerPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
Assert.IsNotNull(args.Field, "args.Field is null");
Assert.IsNotNull(args.ItemSerializer, "args.ItemSerializer is null");
args.Result = new MultiListFieldSerializer(args.ItemSerializer, FieldRenderer, _mediaManager, _multisiteService, _serializedTargetItemService);
}
}
}

Add a Custom MultiListFieldSerializer

using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Sitecore.Abstractions;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.LayoutService.Serialization;
using Sitecore.LayoutService.Serialization.ItemSerializers;
using Sitecore.Links;
using Website.Foundation.LayoutService.Services;
namespace Website.Foundation.LayoutService.Serialization.FieldSerializers
{
public class MultilistFieldSerializer : Sitecore.LayoutService.Serialization.FieldSerializers.MultilistFieldSerializer
{
private readonly ISerializedTargetItemService _serializedTargetItemService;
public MultilistFieldSerializer(
IItemSerializer itemSerializer,
IFieldRenderer fieldRenderer,
BaseMediaManager mediaManager,
ISerializedTargetItemService serializedTargetItemService) : base(itemSerializer, fieldRenderer, mediaManager)
{
_serializedTargetItemService = serializedTargetItemService;
}
protected override string GetSerializedTargetItem(Item item, MultilistField field, int depth)
{
JObject? jObject = _serializedTargetItemService.GetSerializedTargetItemFields(item, depth);
if (jObject != null)
{
return jObject.ToString();
}
return base.GetSerializedTargetItem(item, field, depth);
}
}
}
using Newtonsoft.Json.Linq;
using Sitecore.Data.Items;
using Website.Foundation.LayoutService;
using Website.Foundation.LayoutService.Repositories;
namespace Website.Project.Main.Repositories
{
public class SerializedItemFieldsRepository : ISerializedItemFieldsRepository
{
...
public JObject? GetSerializedItemFields(Item item, int depth)
{
// These are random IDs for example purposes
if (item.TemplateID == new ID("{3CCEB664-AAC5-40D5-BB19-7FFF94C619B5}"))
{
return GetFields(item, new[] {
new ID("{0BEB9157-C202-4385-ABB1-0FE2CA2AA0A4}"),
new ID("{AB10475E-D72B-4DD5-B068-24BAAE87A4BD}")
}, depth);
}
return null;
}
private JObject GetFields(Item item, ID[]? fieldIds, int depth)
{
var serializationOptions = new SerializationOptions { DisableEditing = true };
if (fieldIds != null)
{
return JObject.Parse(_itemSerializer.SerializeFields(item, fieldIds, serializationOptions, depth));
}
return JObject.Parse(_itemSerializer.Serialize(item, serializationOptions));
}
}
}

Add a CustomItemSerializer

<layoutService>
<configurations>
<config name="jss">
<rendering type="Sitecore.LayoutService.Configuration.DefaultRenderingConfiguration, Sitecore.LayoutService">
<itemSerializer patch:instead="*[@type='Sitecore.JavaScriptServices.ViewEngine.LayoutService.JssItemSerializer, Sitecore.JavaScriptServices.ViewEngine']" type="Website.Foundation.LayoutService.Serialization.ItemSerializers.CustomItemSerializer, Website" resolve="true">
<AlwaysIncludeEmptyFields>true</AlwaysIncludeEmptyFields>
</itemSerializer>
</rendering>
</config>
</configurations>
</layoutService>
using System.IO;
using Newtonsoft.Json;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.JavaScriptServices.ViewEngine.LayoutService;
using Sitecore.LayoutService.Serialization;
using Sitecore.LayoutService.Serialization.Pipelines.GetFieldSerializer;
namespace Website.Foundation.LayoutService.Serialization.ItemSerializers
{
public class CustomItemSerializer : JssItemSerializer, ICustomItemSerializer
{
public CustomItemSerializer(IGetFieldSerializerPipeline getFieldSerializerPipeline) : base(getFieldSerializerPipeline)
{
}
// Add ability to serialize a defined set of fields.
public virtual string SerializeFields(Item item, ID[] fieldIds, SerializationOptions options, int depth)
{
using (StringWriter stringWriter = new StringWriter())
{
using (JsonTextWriter jsonTextWriter = new JsonTextWriter(stringWriter))
{
jsonTextWriter.WriteStartObject();
foreach (ID fieldId in fieldIds)
{
Field? itemField = item.Fields[fieldId];
if (itemField != null)
{
SerializeField(itemField, jsonTextWriter, options, depth);
}
}
jsonTextWriter.WriteEndObject();
}
return stringWriter.ToString();
}
}
}
}
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Sitecore.Data.Items;
using Website.Foundation.LayoutService.Repositories;
namespace Website.Foundation.LayoutService.Services
{
public class SerializedTargetItemService : ISerializedTargetItemService
{
private readonly IEnumerable<ISerializedItemFieldsRepository> _serializedItemFieldsRepositories;
public SerializedTargetItemService(
IEnumerable<ISerializedItemFieldsRepository> serializedItemFieldsRepositories)
{
_serializedItemFieldsRepositories = serializedItemFieldsRepositories;
}
public JObject? GetSerializedTargetItemFields(Item item, int depth)
{
var jobj = new JObject();
foreach (ISerializedItemFieldsRepository serializedItemFieldsRepository in _serializedItemFieldsRepositories)
{
JObject? contents = serializedItemFieldsRepository.GetSerializedItemFields(item, depth);
if (contents != null)
{
jobj.Merge(contents);
}
}
if (jobj.Count > 0)
{
return jobj;
}
return null;
}
}
}

Another Approach / More Reading

https://stackoverflow.com/questions/70503440/how-to-override-the-4mb-api-routes-body-size-limit/70504864#70504864

5468 11:26:36 ERROR [JSS] Error occurred during POST to remote rendering host: `http://localhost:3000/api/editing/render`
5468 11:26:36 ERROR The remote server returned an error: (413) Body exceeded 2mb limit.
Exception: System.Net.WebException
Message: The remote server returned an error: (413) Body exceeded 2mb limit.
Source: System
at System.Net.WebClient.UploadDataInternal(Uri address, String method, Byte[] data, WebRequest& request)
at System.Net.WebClient.UploadString(Uri address, String method, String data)
at Sitecore.JavaScriptServices.ViewEngine.Http.RenderEngine.Invoke[T](String moduleName, String functionName, Object[] functionArgs)

See if your solution contains this, and modify the value:

// Bump body size limit (1mb by default) for Sitecore editor payload
// See https://nextjs.org/docs/api-routes/api-middlewares#custom-config
export const config = {
api: {
bodyParser: {
sizeLimit: '2mb',
},
},
};

Keep on building,

Marcel


More Posts