<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:
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:
Together with page appearance in browser, it might look like this:
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:
To achieve it, the following actions are required:
window.actionName
valueBy 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
};
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'));
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>
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
What could be the cause of a made request?
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'
));
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'
));
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>
);
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 })
);
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;