<aside> 🧑‍🎓 This guide refers to critical request chains and critical rendering path. Refer to these guide to learn how to identify them!

How to identify critical rendering path?

How to identify critical request chain?

</aside>

For most pages, the critical content of that page is coming from a unique content of that page, for example:

Untitled

The header, footer, and other miscellaneous components are never critical therefore, they should be deprioritized. By default, these non-critical components are loaded along side the critical request chain:

Untitled

Together with page appearance in browser, it might look like this:

Untitled

The goal of non-critical request de-prioritization is to move non-critical requests made during the critical request chain load to after the critical request chain load. Note, the LCP element rendering happening earlier, due to more resources (bandwidth) available for critical request chain to execute:

Untitled

To achieve it, the following actions are required:

1. Hard-code the default window.actionName value

By default (in Magento mode), ScandiPWA sets the window.actionName.type to determine the page type in HTML entry-point. In production, this entry-point is public/index.php. In development, the public/index.html is used.

It is recommended to hard-code the window.actionName default value for development. To do it, fully override (copy the original content) the public/index.html and add the following script into it:

window.actionName = {
	type: 'CATEGORY' // <- set the type, based on the page your are looking at
};

2. Set a default flag value, based on page type

Completely override (copy it’s original content) the entry-point (src/index.js), add the preload detection logic before the app is rendered:

/* eslint-disable simple-import-sort/sort */

import 'Util/Polyfill';
import 'Style/main';

import { render } from 'react-dom';

import App from 'Component/App';

// vvv add this section
export const isPriorityLoadedByDefault = (
    window.actionName?.type !== 'CATEGORY'
    && window.actionName?.type !== 'PRODUCT'
    && window.actionName?.type !== 'CMS_PAGE'
		// Additional page types that need deprioritizing can be added here
);

// vvv Set to false, aka. make app wait for priority load ONLY for given pages
window.isPriorityLoaded = isPriorityLoadedByDefault;
// ^^^ end of added section

// let's register service-worker
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        const swUrl = '/service-worker.js';
        navigator.serviceWorker.register(swUrl, { scope: '/' });
    });
}

render(<App />, document.getElementById('root'));

3. Create a promise that resolves once the flag is set

Create a utility function alike within your application:

const RETRY_DELAY = 50;
export const EV_PRIORITY_LOAD_END = 'pr_end';

export const waitForPriorityLoad = () => new Promise((resolve) => {
  function waitForIt() {
    if (window.isPriorityLoaded) {
			const ev = new Event(EV_PRIORITY_LOAD_END);
	    document.dispatchEvent(ev);
      resolve();
      return;
    }

    setTimeout(waitForIt, RETRY_DELAY);
  }

  waitForIt();
});

<aside> 🧠 Since ScandiPWA 6.1.0 it is bundled with the app, in Util/Request/LowPriorityLoad

</aside>

4. De-prioritize layout-level components

Do the complete override (copying the original file) the Router component, then, make all components that are not pages non-critical. Replace lazy import of such components to lowPriorityLazy. For example, the dynamic-import section of Router component could look like this:

// vvv non-critical components set to load with low priority
export const CookiePopup = lowPriorityLazy(() => import(/* webpackMode: "lazy", webpackChunkName: "nav" */ 'Component/CookiePopup'));
export const Header = lowPriorityLazy(() => import(/* webpackMode: "lazy", webpackChunkName: "nav" */ 'Component/Header'));
export const NavigationTabs = lowPriorityLazy(() => import(/* webpackMode: "lazy", webpackChunkName: "nav" */ 'Component/NavigationTabs'));
export const Footer = lowPriorityLazy(() => import(/* webpackMode: "lazy", webpackChunkName: "footer" */ 'Component/Footer'));

// vvv critical components left as they where declared
export const SomethingWentWrong = lazy(() => import(/* webpackMode: "lazy", webpackChunkName: "error" */ 'Route/SomethingWentWrong'));
export const CartPage = lazy(() => import(/* webpackMode: "lazy", webpackChunkName: "cart" */ 'Route/CartPage'));
export const Checkout = lazy(() => import(/* webpackMode: "lazy", webpackChunkName: "checkout" */ 'Route/Checkout'));
export const CmsPage = lazy(() => import(/* webpackMode: "lazy", webpackChunkName: "cms" */ 'Route/CmsPage'));
export const HomePage = lazy(() => import(/* webpackMode: "lazy", webpackChunkName: "cms" */ 'Route/HomePage'));

// TODO: deprioritize router

5. De-prioritize page-specific components / requests

What could be the cause of a made request?

How to de-prioritize dynamic imports?

A dynamic import, is a import made using the import() syntax, for example:

const CartDispatcher = import(
/* webpackMode: "lazy", webpackChunkName: "cart" */
  'Store/Cart/Cart.dispatcher'
);

To make this dynamic import await the promise, we need to await the flag, and then attempt the import, like so:

const CartDispatcher = waitForPriorityLoad().then(() => import(
    /* webpackMode: "lazy", webpackChunkName: "cart" */
    'Store/Cart/Cart.dispatcher'
));

How to de-prioritize lazy components?

A lazy component, is a dynamically-imported component, wrapped with a lazy React utility:

import { lazy } from 'react';

const CartOverlay = lazy(() => import(
	/* webpackMode: "lazy", webpackChunkName: "overlay" */
	'Component/CartOverlay'
));

To make lazy component wait until the flag is set, we need to modify the promise passed as callback into the lazy function, awaiting the flag inside. To avoid doing it every time, declare the following utility function within your application:

export const lowPriorityLazy = (callback) => lazy(async () => {
  await waitForPriorityLoad();
  return callback();
});

<aside> 🧠 Since ScandiPWA 6.1.0 it is bundled with the app, in Util/Request/LowPriorityLoad

</aside>

Now, use modify the original lazy component declaration to use this function, over original lazy:

const CartOverlay = lowPriorityLazy(() => import(
	/* webpackMode: "lazy", webpackChunkName: "overlay" */
	'Component/CartOverlay'
));

How to de-prioritize rendering of a component?

Similar to React Suspense we need an API to display fallback until the critical load flag is set, then we can safely render children. Declare the following component within your application:

import { useState, useEffect } from 'react';

export function AfterPriority({ children, fallback = null }) {
  const [isPriorityLoaded, setIsPriorityLoaded] = useState(window.isPriorityLoaded);

  function onPriorityLoad() {
    setIsPriorityLoaded(true);
  }

  useEffect(() => {
    document.addEventListener(EV_PRIORITY_LOAD_END, onPriorityLoad, { once: true });

    return () => {
      document.removeEventListener(EV_PRIORITY_LOAD_END, onPriorityLoad);
    };
  }, []);

  if (!isPriorityLoaded) {
    return fallback;
  }

  return children;
}

<aside> 🧠 Since ScandiPWA 6.1.0 it is bundled with the app, in Util/Request/LowPriorityLoad

</aside>

You can now use it to wrap the components you do not want to render, until critical chain load:

if (i === 0) {
  // always render first image
	return this.renderImage(img);
}

return (
	<AfterPriority fallback={ <div /> }>
		{ this.renderImage(img) }
  </AfterPriority>
);

How to de-prioritize a function call?

There are cases, where we might want to delay a data to request made within some function until the critical request chain load flag is set. For example, we want to de-prioritize filter request:

requestProductListInfo({ ...infoOptions, getSortOnly: true })

All we need to do, is await the flag and then, execute the function:

waitForPriorityLoad().then(
  () => requestProductListInfo({ ...infoOptions, getSortOnly: true })
);

6. Set a flag to indicate the end of the critical request chain load

Let’s base our implementation with simple flag set on global object. We will call this each time we intend to set the "critical request chain loaded" flag:

window.isPriorityLoaded = true;

<aside> 🧠 When does the critical request chain end? Once the last request is finished. Most critical chains end with LCP media element load, therefore, to track the end of critical request chain load, we need to track the load of the media element.

</aside>

A simple solution would be to set this flag once the first image loads. This, however has its downside: it requires LCP media to be the only media present on page during critical load.

To achieve a more granular control, it is possible to add an option onLoad prop to an image, and pass the logic to set the flag from outside: product gallery for PDP, category details or product cards for PLP. The implementation will then look as follows:

import { Image as SourceImage } from 'SourceComponent/Image/Image.component';

export class Image extends SourceImage {
	onLoad(...args) {
		super.onLoad(...args);
		
		const { onLoad } = this.props;
		if (onLoad) onLoad();
	}
}

export default Image;

Then, in product gallery base image (so, for PDP critical chain) we can do the following:

import {
    ProductGalleryBaseImage as SourceProductGalleryBaseImage
} from 'SourceComponent/ProductGalleryBaseImage/ProductGalleryBaseImage.component';

export class ProductGalleryBaseImageComponent extends SourceProductGalleryBaseImage {
	render() {
    const { src, alt } = this.props;

    return (
      <Image
        src={ src }
        alt={ alt }
        onLoad={ () => {
					// On load, set the "critical request chain loaded" flag
          window.isPriorityLoaded = true;
        } }
      />
		);
}

export default ProductGalleryBaseImageComponent;