Increase your Sitecore JSS performance with the Hybrid Placeholder

When you're working with Sitecore JSS you can use a custom Rendering Contents Resolver. This offers some great flexibility because you have full control over the JSON output for your component. On a page you usually have multiple placeholders which each have multiple components. So you might have a lot of custom Rendering Contents Resolvers. All of these need to be executed before the page is loaded. Even if you have a SPA and use client-side routing Sitecore JSS links it still needs to load all of them first before the new page is rendered. This usually isn't a problem, but if you have a custom Rendering Contents Resolver which is slow (for example because you need to fetch some 3rd party data) the user always needs to wait for it before anything happens. On an SPA this means the user doesn't get any feedback until everything is ready. Since it's not an actual page load the browser doesn't give a loading indicator by default.

For an SPA it would be useful if the custom Rendering Contents Resolver could load the slow parts later. So the page is already loaded and then does another call to get the rest of the data. This could be done by moving the heavy code from the custom Rendering Contents Resolver to a separate Web API. So when the page is loaded another request is done to the Web API to get the rest of the data. This works, but it could be a lot of extra work to build a separate Web API and also the code to fetch it in Javascript. Also see the docs about this: Why not a separate REST endpoint?. It would be easier if the heavy parts of the custom Rendering Contents Resolver could be loaded later from the same code. This is possible with the Hybrid Placeholder. Here is an example of how it looks. Increase your Sitecore JSS performance with the Hybrid Placeholder

By using the Hybrid Placeholder you can add an if statement around your heavy code in your custom Rendering Contents Resolver which will be executed async after the page is already loaded. This is done by using the fetchPlaceholderData API. This API was first mentioned in Adam his blog and can be used for multiple scenarios. I came up with a Hybrid Placeholder component to replace the default Placeholder in React. In the Hybrid Placeholder you already have the JSON from the custom Rendering Contents Resolver and that can already be rendered. But it will also get the placeholder data again aysnc. This time with the heavy code. After it's ready it will use the updated JSON to render the components again which now includes the JSON from the heavy code. This is the code of the Hybrid Placeholder React functional component:

import React, { useEffect, useState } from 'react'; 
import { withSitecoreContext, dataApi, Placeholder } from '@sitecore-jss/sitecore-jss-react';
import { dataFetcher } from './dataFetcher';
import config from './temp/config';

const HybridPlaceholder = ({
  name,
  rendering,
  sitecoreContext,
}) => {
  const {
    route,
    pageEditing,
  } = sitecoreContext;

  const [isFetched, setIsFetched] = useState(false);

  // Used to fetch the placeholder data with specific parameters.
  const fetchPlaceholder = () => dataApi.fetchPlaceholderData(
    name,
    route?.itemId,
    {
      layoutServiceConfig: {
        host: config.sitecoreApiHost,
      },
      querystringParams: {
        sc_lang: route?.itemLanguage,
        sc_apikey: config.sitecoreApiKey,
        isHybridPlaceholder: true,
      },
      fetcher: dataFetcher,
    },
  );

  // Will add the isLoaded prop to all components.
  const addIsLoadedProp = (isLoaded, elements) => {
    if (Array.isArray(elements)) {
      elements.forEach(({ fields }) => {
        if (fields) {
          fields.isLoaded = isLoaded;
        }
      });
    }
  };

  // Only fetch the placeholder data when we navigate to a new page.
  // Since useEffect does not work server-side we don't need a client-side check.
  useEffect(() => {
    if (!pageEditing && rendering?.placeholders?.[name]) {
      setIsFetched(false);
      fetchPlaceholder()
        .then(result => {
          addIsLoadedProp(true, result.elements);
          // Override all components in the placeholder with the new data.
          // This data contains the heavy code.
          rendering.placeholders[name] = result.elements;
          setIsFetched(true);
        }).catch(error => {
          console.error(error);
        });
    }
  }, [route?.itemId]);

  if (!pageEditing
      && !isFetched
      && rendering?.placeholders?.[name]) {
    addIsLoadedProp(false, rendering.placeholders[name]);
  }

  return (
    // Render the first time without the heavy data.
    // Render a second time with all the data loaded.
    <Placeholder name={name} rendering={rendering} />
  );
};

export default withSitecoreContext()(HybridPlaceholder);

Once you use the Hybrid Placeholder the custom Rendering Contents Resolver will be called twice. The second time with an extra querystring parameter which can be used to determine if the heavy code should be executed. Here is an updated example from my previous blog:

public class PromoRenderingContentsResolver : IRenderingContentsResolver
{
    private readonly Func<IMvcContext> contextThunk;

    private readonly IMapper mapper;

    public PromoRenderingContentsResolver(Func<IMvcContext> contextThunk, IMapper mapper)
    {
        this.contextThunk = contextThunk;
        this.mapper = mapper;
    }

    public override object ResolveContents(Rendering rendering, IRenderingConfiguration renderingConfig)
    {
        var context = this.contextThunk();
        var datasource = context.GetDataSourceItem<Promo>();
        var model = this.mapper.Map<PromoJsonDto>(datasource);
        if (this.IsHybridPlaceholder)
        {
            // Here the heavy code can be executed which will be done async.
            // So after the page is already loaded this will be added afterwards.
            Thread.Sleep(2000);
            model.Date = DateTime.Now.ToString("f");
        }

        return model;
    }

    private bool IsHybridPlaceholder
    {
        get
        {
            bool.TryParse(HttpContext.Current?.Request?.QueryString?["isHybridPlaceholder"], out var isHybridPlaceholder);
            return isHybridPlaceholder;
        }
    }
}

By using the Hybrid Placeholder component an extra prop is added to the component. With isLoaded you can check if the second call has been executed. If it's not you can show a loading text or indicator.

const PromoBlock = (props) => {
    const {
        image,
        richText,
        heading,
        button,
        isLoaded
    } = props;

    return (
        <Theme>
            <Layer>
                <Retain>
                    { isLoaded && (
                        <p>Date: {props.date}</p>
                    )}
                    { !isLoaded && (
                        <p>Date: Loading...</p>
                    )}
                    <div className="c-promoblock">
                        <div className="o-layout  o-layout--gutter">
                            <div className="o-layout__cell  u-fraction--1/2@from-lap">
                                <Img image={image} className="u-m-b" />
                            </div>
                            <div className="o-layout__cell  u-fraction--1/2@from-lap">
                                <Heading text={heading.text} level={heading.level} className="u-m-t-tiny" />
                                <Rte richText={richText} />
                                {button ? (
                                    <Button tag="button" modifier="secondary" field={button} />
                                ) : null}
                            </div>
                        </div>
                    </div>
                </Retain>
            </Layer>
        </Theme>
    );
};

And that's it! With this is possible to load parts of a custom Rendering Contents Resolver async after the page is already loaded.

This is a simplified version of what we're using for our customers. In the full version the following is also supported:

  • Server-Side Rendering support. The heavy parts are only loaded async when using Client-Side Rendering with XHR. If it's a normal request to the server all data is loaded.
  • Only load the heavy parts async if it's enabled in one of the custom Rendering Contents Resolvers.
  • All functionality is part of a base class and everything supports Dependency Injection.

Creating the Hybrid Placeholder was only possible because of the fetchPlaceholderData API. It has been part of Sitecore JSS for a while, but not really used anywhere. The Hybrid Placeholder is just an example of what can be done with it. There are probably other use cases as well. I hope more people will start using it now.

You can find the code on GitHub here: https://gist.github.com/jbreuer/b63cd87a9b15ff1f660cb8847413d1d8