<aside> 🧠You should already be familiar with how the data is preloaded in the document. Read more about it here:
</aside>
add a new entry to actionName
in index.php
window.actionName = {
slider: <?= json_encode($this->getSlider()); ?> || {}
};
extend the Page.php
by preferencing the ScandiPWA\\Router\\View\\Result\\Page
class to add new property slider
, where we will store our preloaded slider data.
<aside> 🧠if you are unfamiliar with a Magento 2 preference, follow the following instructions
</aside>
use ScandiPWA\\Customization\\View\\Result\\Page as SourcePage;
class Page extends SourcePage
{
/**
* @var array|null
*/
protected $slider;
public function __construct(
// __construct arguments here
) {
// call parent __construct here
$this->slider = null;
}
/**
* Set preloaded slider
* @param string
* @return \\Magento\\Framework\\View\\Result\\Page
*/
public function setSlider($slider)
{
if ($this->slider === null) {
$this->slider = $slider;
return $this;
}
return null;
}
/**
* Retrieve preloaded slider
*
* @return string
*/
public function getSlider()
{
return $this->slider;
}
set the preloaded slider data on an action in RouterPlugin
in this example we will set the slider data for a homepage, but it can be set on any page type ( e.g. CMS page ).
protected function setResponseHomePage(ActionInterface $action, $homePageIdentifier)
{
try {
$page = $this->pageProvider->getDataByPageIdentifier($homePageIdentifier, (int)$this->storeId);
$action->setType(self::PAGE_TYPE_CMS_PAGE);
$action->setId($page['page_id'] ?? '');
$action->setPage($page);
$action->setSlider($this->getSliderInformation($page['content'] ?? ''));
} catch (\\Throwable $th) {
$this->setNotFound($action);
}
}
create a getSliderInformation
method, which will return the slider data
use Magento\\Framework\\Filter\\Template\\Tokenizer\\Parameter as TokenizerParameter;
use ScandiPWA\\SliderGraphQl\\Model\\Resolver\\Slider as SliderResolver;
class RouterPlugin
{
protected TokenizerParameter $tokenizerParameter;
protected SliderResolver $sliderResolver;
public function __construct(
TokenizerParameter $tokenizerParameter,
SliderResolver $sliderResolver
)
{
$this->tokenizerParameter = $tokenizerParameter;
$this->sliderResolver = $sliderResolver;
}
/**
* @param string $content
*
* This function gets first widget from page content
* In case if it is slider, then it gets slider it.
*/
protected function getSliderInformation($content)
{
preg_match('/{{(.*?)}}/m', $content, $match);
$this->tokenizerParameter->setString($match[0] ?? '');
$params = $this->tokenizerParameter->tokenize();
if (isset($params['slider_id'])) {
return $this->sliderResolver->getSlider($params['slider_id']);
}
}
}
Preload the slider chunk
locate the slider chunk. by default, in the ScandiPWA core, the slider widget can be found in WidgetFactory
component. In this case, the widget-slider
chunk will need to be preloaded.
export const HomeSlider = lazy(() => import(
/* webpackMode: "lazy", webpackChunkName: "widget-slider" */
'Component/SliderWidget'
));
add widget-slider
chunk to preload configuration’s cacheGroupWhitelist
<aside> 🧠Read more about preload configuration here:
How to preload critical chunks?
</aside>
const cacheGroupWhitelist = ['widget-slider', 'render', 'cms', 'product', 'category'];
add widget-slider
chunk to chunkValidator
to generate a preload link for it
const chunkValidator = {
// other chunks
'widget-slider': window.actionName.type === 'CMS_PAGE' && Object.keys(window?.actionName?.slider || {}).length,
}
Prevent unnecessary request, if the slider data has already been preloaded
<aside> 🧠if you are unfamiliar with ScandiPWA components and override mechanism read more about it here:
</aside>
extend the SliderWidget.container
’s requestSlider
method and add a condition that will check whether the requested slider id matches the one that has been preloaded in the document:
requestSlider() {
const { sliderId } = this.props;
const {
actionName: {
slider: {
slider_id: preloadedSliderId
} = {},
slider: preloadedSlider = {}
}
} = window;
if (sliderId === Number(preloadedSliderId)) {
this.setState({ slider: preloadedSlider });
return;
}
// slider data hasn't been preloaded, make a request for it
return super.requestSlider();
}
<aside> 🎉 We got the slider data early right in the HTML document!
</aside>
at this point the rendering chain should look something similar:
to further optimize the LCP time load, we can deprioritize the slides that aren’t visible. meaning only load the active first slide. to visualize, this is how the render chain should look like:
Let’s override the SliderWidget.component
's renderSlide
method and check whether the slide is currently active or not and wrap it with AfterPriority
component accordingly
<aside>
🧠if you are not familiar with AfterPriority
component, read more about it here:
How to de-prioritize non-critical requests?
</aside>
renderSlideImage(slide, i) {
const {
isPlaceholder,
title: block,
slide_text
} = slide;
return (
<figure
block="SliderWidget"
elem="Figure"
key={ i }
aria-hidden={ i !== this.state.activeImage }
>
<Image
mix={ { block: 'SliderWidget', elem: 'FigureImage' } }
ratio="custom"
src={ this.getSlideImage(slide) }
alt={ block }
isPlaceholder={ isPlaceholder }
onLoad={ setIsPriorityLoadedFlag }
/>
<figcaption
block="SliderWidget"
elem="Figcaption"
mix={ { block } }
>
<Html content={ slide_text || '' } />
</figcaption>
</figure>
);
}
renderSlide(slide, i) {
const { activeImage } = this.state;
const { isPriorityLoaded } = window;
if (activeImage !== i && !isPriorityLoaded) {
return (
<AfterPriority fallback={ <div /> }>
{ this.renderSlideImage(slide, i) }
</AfterPriority>
);
}
return this.renderSlideImage(slide, i);
}
<aside> 🎉 We got the first slide loaded with priority and further improved the LCP load time
</aside>
even though we have prioritized the first slide as it’s a LCP on a page, we are still waiting for the rendering chunks to be loaded to start downloading the slide image. this can be further improved if we will preload the image beforehand in the document. to visualize it, this is how the rendering path can look like:
Let’s add a preload for it index.php
const appendPreloadLink = (as, href) => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = as;
link.href = href;
document.head.appendChild(link);
}
const {
actionName: {
slider = {},
slider: { slides = {} } = {},
} = {}
} = window;
// Preload for slider first image
if (Object.keys(slider).length) {
const [{ desktop_image, mobile_image }] = slides;
const imageUrl = window.matchMedia('(max-width: 810px)').matches
&& window.matchMedia('screen').matches
? (mobile_image || desktop_image)
: (desktop_image || mobile_image);
appendPreloadLink('image', `/${imageUrl}`);
}
<aside> 🎉 We got the LCP media from the slider widget significantly faster
</aside>