<aside> 🧑🎓 The critical data is used for rendering FCP, eliminate or make critical requests early. More on these concepts can be read in following guides:
How to get FCP and LCP readings?
How to identify critical request chain?
</aside>
Preloading of critical data, can:
To visualize it, consider the following request chain (unoptimized):
Preloading critical data will have the following effect:
To achieve this effect, the following actions are required:
FCP happens too early to rely on data requested from JavaScript. Therefore, it is suggested to inline data required for FCP right into the HTML document, on server side.
<aside> 🧠 To get fast FCP reading, the display of simple, yet contentful page is required. Specifically, the display of placeholders for the desired page type, with some entity-relevant content. Therefore, it is crucial to detect which type of page is opened early. Knowing a page type, we can start loading necessary data to render it. The necessary data could be, for example:
The template used to generate HTML document on server side, is located in public
folder, in Magento theme mode, the public/index.php
template is used. Modify it, to get critical data early. The following code to be extended with additional data (in addition to already provided page type):
window.actionName = {
type: `<?= $this->getAction(); ?>`
};
Modify it, to look as follows:
window.actionName = {
type: `<?= $this->getAction(); ?>`,
id: parseInt(`<?= $this->getId(); ?>`) || null,
sku: `<?= $this->getSku(); ?>` || null,
name: `<?= $this->getName(); ?>`,
page: <?= json_encode($this->getPage()); ?> || {},
identifier: `<?= $this->getIdentifier(); ?>`,
description: `<?= $this->getDescription(); ?>`
};
<aside>
🧠 During development (or in storefront mode) the public/index.html
template is used. Feel free to hardcode desired window.actionName
values in it. The real data will be put into the actionName
only when the application is ran in Magento theme mode.
</aside>
The context of this
in this template, is defined in ScandiPWA\\Router\\Controller\\Pwa
. The ScandiPWA\\Router\\Controller\\Router
is matching routes and is capable of obtaining necessary entity details to output into the HTML document. To provide necessary data, add setters to Pwa
controller and call them with corresponding data from Router
controller.
Create View/Result/Page.php
and populate with:
<?php
namespace Scandiweb\\Router\\View\\Result;
use Magento\\Framework\\App\\Filesystem\\DirectoryList;
use Magento\\Framework\\Exception\\FileSystemException;
use Magento\\Framework\\Exception\\NoSuchEntityException;
use Magento\\Framework\\Filesystem\\Driver\\File;
use Magento\\Framework\\Locale\\Resolver;
use Magento\\Framework\\Serialize\\Serializer\\Json;
use Magento\\Framework\\Translate\\InlineInterface;
use Magento\\Framework\\View\\Element\\Template\\Context;
use Magento\\Framework\\View\\EntitySpecificHandlesList;
use Magento\\Framework\\View\\Layout\\BuilderFactory;
use Magento\\Framework\\View\\Layout\\GeneratorPool;
use Magento\\Framework\\View\\Layout\\ReaderPool;
use Magento\\Framework\\View\\LayoutFactory;
use Magento\\Framework\\View\\Page\\Config\\RendererFactory;
use Magento\\Framework\\View\\Page\\Layout\\Reader;
use Magento\\Store\\Model\\StoreManagerInterface;
use ScandiPWA\\Customization\\Controller\\AppIcon;
use ScandiPWA\\Customization\\View\\Result\\Page as SourcePage;
use Scandiweb\\Migrations\\Helper\\StoreHelper;
/**
* Class Page
*
* @package Scandiweb\\Customization\\View\\Result
*/
class Page extends SourcePage
{
/**
* @var string
*/
protected $id;
/**
* @var string
*/
protected $sku;
/**
* @var string
*/
protected $name;
/**
* @var array|null
*/
protected $page;
/**
* @var array|null
*/
protected $post;
/**
* @var string|null
*/
protected $imageUrl;
/**
* @var string
*/
protected $description;
/**
* @var array|null
*/
protected $storeConfig;
/**
* Page constructor.
*
* @param StoreManagerInterface $storeManager
* @param Resolver $localeResolver
* @param Context $context
* @param LayoutFactory $layoutFactory
* @param ReaderPool $layoutReaderPool
* @param InlineInterface $translateInline
* @param BuilderFactory $layoutBuilderFactory
* @param GeneratorPool $generatorPool
* @param RendererFactory $pageConfigRendererFactory
* @param Reader $pageLayoutReader
* @param DirectoryList $directoryList
* @param Json $json
* @param string $template
* @param AppIcon $appIcon
* @param bool $isIsolated
* @param EntitySpecificHandlesList|null $entitySpecificHandlesList
* @param null $action
* @param array $rootTemplatePool
*/
public function __construct(
StoreManagerInterface $storeManager,
Resolver $localeResolver,
Context $context,
LayoutFactory $layoutFactory,
ReaderPool $layoutReaderPool,
InlineInterface $translateInline,
BuilderFactory $layoutBuilderFactory,
GeneratorPool $generatorPool,
RendererFactory $pageConfigRendererFactory,
Reader $pageLayoutReader,
DirectoryList $directoryList,
Json $json,
string $template,
AppIcon $appIcon,
$isIsolated = false,
EntitySpecificHandlesList $entitySpecificHandlesList = null,
$action = null,
$rootTemplatePool = []
) {
parent::__construct(
$storeManager,
$localeResolver,
$context,
$layoutFactory,
$layoutReaderPool,
$translateInline,
$layoutBuilderFactory,
$generatorPool,
$pageConfigRendererFactory,
$pageLayoutReader,
$directoryList,
$json,
$template,
$appIcon,
$isIsolated,
$entitySpecificHandlesList,
$action,
$rootTemplatePool
);
$this->id = '';
$this->sku = '';
$this->name = '';
$this->page = null;
$this->identifier = '';
$this->description = '';
}
public function setId(string $id)
{
if($this->id === '') {
$this->id = $id;
return $this;
}
return '';
}
public function getId()
{
return $this->id;
}
public function setSku(string $sku)
{
if($this->sku === '') {
$this->sku = $sku;
return $this;
}
return '';
}
public function getSku()
{
return $this->sku;
}
public function setName(string $name)
{
if($this->name === '') {
$this->name = $name;
return $this;
}
return '';
}
public function getName()
{
return $this->name;
}
public function setPage($page)
{
if($this->page === null) {
$this->page = $page;
return $this;
}
return null;
}
public function getPage()
{
return $this->page;
}
public function setIdentifier(string $identifier)
{
if ($this->identifier === '') {
$this->identifier = $identifier;
return $this;
}
return '';
}
public function getIdentifier()
{
return $this->identifier;
}
public function setDescription(string $description)
{
if ($this->description === '') {
$this->description = $description;
return $this;
}
return '';
}
public function getDescription()
{
return $this->description;
}
}
<aside> 🎉 We got the data early right in the HTML document!
</aside>
First, override a src/store/index.js
. Inside, remove critical reducers from the getStaticReducers
function, for example: ProductListReducer
and ProductReducer
. Make sure to remove an import too.
Next, add a logic to inject critical reducers early in src/index.js
(entry-point). start by creating util in src/util/Store/index.js
export * from 'SourceUtil/Store';
export { default } from 'SourceUtil/Store';
export function injectReducers(store, reducers) {
// Inject all the static reducers into the store
Object.entries(reducers).forEach(
([name, reducer]) => {
if (store.injectReducer) {
store.injectReducer(name, reducer);
}
},
);
return store;
}
then use this util to inject priority reducers immediately:
import ProductReducer from 'Store/Product/Product.reducer';
import ProductListReducer from 'Store/ProductList/ProductList.reducer';
import { getStore } from 'Util/Store';
const store = getStore();
injectReducers(store, {
ProductReducer,
ProductListReducer
});
Also it is required to override App.component.js
to adjust store configuration for app. Where is result it will be injecting reducers same was as early reducers.
import { App as SourceAppComponent } from 'SourceComponent/App/App.component';
import { getStaticReducers } from 'Store/index';
import getStore, { injectReducers } from 'Util/Store';
export class AppComponent extends SourceAppComponent {
configureStore() {
const store = getStore();
injectReducers(store, getStaticReducers());
this.reduxStore = store;
}
}
export default AppComponent;
Now, given the page type is known, we can preload necessary data required to render the page. To do, we need to check the page and execute fetch of the corresponding data.
To begin, add the following code right after imports in the src/index.js
(entry-point):
switch (window.actionName.type) {
case 'CATEGORY':
prefetchCategory();
break;
case 'PRODUCT':
prefetchProduct();
break;
}
<aside> 🧠 Preloading of homepage data from JavaScript is not required, the data is already present in HTML document.
</aside>
Now, let’s focus on functions to preload the data for each page:
Create a utility function alike within your application:
import ProductDispatcher from 'Store/Product/Product.dispatcher';
import getStore from 'Util/Store';
export const prefetchProduct = () => {
ProductDispatcher.handleData(
getStore().dispatch,
{
isSingleProduct: true,
args: {
filter: {
// vvv can be either product id or sku
// productID: window.actionName?.id
productSKU: window.actionName?.sku
}
}
}
);
}
Create a utility function alike within your application:
import ProductListDispatcher from 'Store/ProductList/ProductList.dispatcher';
import history from 'Util/History';
import getStore from 'Util/Store';
import { getQueryParam } from 'Util/Url';
export const prefetchCategory = () => {
const { location } = history;
const currentPage = +(getQueryParam('page', location) || 1);
const selectedFiltersString = (getQueryParam('customFilters', location) || '').split(';');
const customFilters = selectedFiltersString.reduce((acc, filter) => {
if (!filter) {
return acc;
}
const [key, value] = filter.split(':');
return { ...acc, [key]: value.split(',') };
}, {});
const min = +getQueryParam('priceMin', location);
const max = +getQueryParam('priceMax', location);
ProductListDispatcher.handleData(
getStore().dispatch,
{
isNext: false,
isPlp: true,
noAttributes: false,
noVariants: false,
args: {
sort: {
sortDirection: getQueryParam('sortDirection', location) || 'ASC',
sortKey: getQueryParam('sortKey', location) || 'position'
},
filter: {
priceRange: { min, max },
customFilters,
categoryIds: window.actionName.id
},
search: '',
pageSize: 24,
currentPage
}
}
);
}
The URL rewrites state is kept in Redux. Initially, it is empty, therefore the URL rewrites request is made to populate it. Given the data from HTML document, it is possible for us to pre-populate the url rewrites state, to avoid making the initial request. To achieve it, replace the initial state of UrlRewrites.reducer
to:
import { getInitialState as sourceGetInitialState } from 'SourceStore/UrlRewrites/UrlRewrites.reducer';
export const getInitialState
= () => ({
...sourceGetInitialState(),
// vvv Assume last requested url rewrite was on the current page
requestedUrl: location.pathname,
// vvv Construct URL rewrite from minimal data from HTML document
urlRewrite: {
id: window.actionName.id,
sku: window.actionName.sku,
type: window.actionName.type,
},
});
export * from 'SourceStore/UrlRewrites/UrlRewrites.reducer';
import { connect } from 'react-redux';
import {
mapDispatchToProps,
mapStateToProps,
UrlRewritesContainer as SourceUrlRewriteContainer
} from 'SourceRoute/UrlRewrites/UrlRewrites.container';
/** @namespace Scandipwa/Route/UrlRewrites/Container/UrlRewritesContainer */
export class UrlRewritesContainer extends SourceUrlRewriteContainer {
__construct() {
if (this.getIsLoading()) {
this.requestUrlRewrite();
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UrlRewritesContainer);
// TODO: complete once we are sure about window.isPrefetchValueUsed
// TODO: complete once we are sure about window.isPrefetchValueUsed
The CMS page does not use Redux (until version 6.1.0
). We need to replicate the CMS page load right when component is mounted. To achieve it, modify CmsPage.container
like so:
import { connect } from 'react-redux';
import {
mapStateToProps,
mapDispatchToProps,
CmsPageContainer as SourceCmsPageContainer
} 'SourceRoute/CmsPage/CmsPage.container';
export class CmsPageContainer extends SourceCmsPageContainer {
requestPage() {
const params = this.getRequestQueryParams();
const { id, identifier = '' } = params;
if (
id === window.actionName.id
|| identifier.includes(window.actionName.page?.identifier)
) {
// vvv Skip making a request, if we have preloaded CMS data
this.onPageLoad({ cmsPage: window.actionName.page });
return;
}
super.requestPage();
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CmsPageContainer);