How to optimize PDP

<aside> ❗ This guide suggests that most of basic optimization is already done, by following next guides:

How to preload critical chunks?

How to split preload and rendering?

How to inline main chunk?

</aside>

Before starting to optimize PDP it is required to take an analysis of the page to determine its critical elements and what should be considered as the high priority element (LCP).

By default, in most cases LCP element on PDP is considered to be one of the main product card image in the viewport.

Untitled

Untitled

How to use preload data on PDP

Preload the product gallery chunk

Locate the product gallery chunk. By default, in the ScandiPWA core, the product gallery can be found in ProductPage component. In this case, the product-gallery chunk will need to be preloaded.

export const ProductGallery = lazy(() => import(
    /* webpackMode: "lazy", webpackChunkName: "product-gallery" */
    'Component/ProductGallery'
));

Add product-gallery chunk to preload configuration’s cacheGroupWhitelist

<aside> 🧠 Read more about preload configuration here: How to preload critical chunks?

</aside>

const cacheGroupWhitelist = ['cms', 'product', 'category', 'render', 'widget-slider', 'product-gallery'];

Add product-gallery chunk to chunkValidator to generate a preload link for it

const chunkValidator = {
	// other chunks
  'product-gallery': window.actionName.type === 'PRODUCT'
}

Optimize the LCP media once the product gallery chunk has been preloaded

To further optimize the LCP time load, We need to load and show the product image and call setIsPriorityLoaded once the product image is loaded.

Let’s override the ProductGallery.component's renderImage method and call setIsPriorityLoaded accordingly.

renderImage(mediaData, index) {
        const {
            isZoomEnabled,
            handleZoomChange,
            disableZoom,
            isMobile,
            isImageZoomPopupActive,
            showLoader
        } = this.props;
        const { scrollEnabled } = this.state;

        if (!isMobile) {
            const {
                base: { url: baseSrc } = {},
                large: { url: largeSrc } = {}
            } = mediaData;

            const style = isImageZoomPopupActive ? { height: 'auto' } : {};
            const src = isImageZoomPopupActive ? largeSrc : baseSrc;

            return (
                <Image
                  key={ index }
                  src={ src }
                  ratio="custom"
                  mix={ {
                      block: 'ProductGallery',
                      elem: 'SliderImage',
                      mods: { isPlaceholder: !src }
                  } }
                  isPlaceholder={ !src }
                  style={ style }
                  showIsLoading={ showLoader }
                  onImageLoad={ setIsPriorityLoaded }
                />
            );
        }

        return (
            <TransformWrapper
              key={ index }
              onZoomChange={ handleZoomChange }
              onWheelStart={ this.onWheelStart }
              onWheel={ this.onWheel }
              wheel={ { limitsOnWheel: true, disabled: !scrollEnabled } }
            //   doubleClick={ { mode: 'reset' } }
              pan={ {
                  disabled: !isZoomEnabled,
                  limitToWrapperBounds: true,
                  velocity: false
              } }
              options={ {
                  limitToBounds: true,
                  minScale: 1
              } }
            >
                { ({
                    scale,
                    previousScale,
                    resetTransform,
                    setTransform
                }) => {
                    if (scale === 1 && previousScale !== 1) {
                        resetTransform();
                    }

                    return (
                        <ProductGalleryBaseImage
                          setTransform={ setTransform }
                          index={ index }
                          mediaData={ mediaData }
                          scale={ scale }
                          previousScale={ previousScale }
                          disableZoom={ disableZoom }
                          isZoomEnabled={ isZoomEnabled }
                        />
                    );
                } }
            </TransformWrapper>
        );
    }

Preload gallery media right into the document

Even though we have prioritized the product image 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.

Let’s add a preload for it index.php

const {
    actionName: {
        imageUrl,
        type
    } = {}
} = window;

if (type === 'PRODUCT' && imageUrl && imageUrl !== '') {
    const link = document.createElement('link');
    link.rel = 'preload';
    link.as = 'image';
    link.href = imageUrl;

    document.head.appendChild(link);
}

How to deprioritize everything but LCP element for PDP

<aside> 🔗 How to de-prioritize non-critical requests?

</aside>

To minimize the size of the product chunk and reduce all unnecessary code that is not considered to be part of the critical rendering path, it should be deprioritized.

It is required to render only LCP elements (usually images) as soon as possible.

Untitled

This means, that on Product page, almost everything can be deprioritized!

Lets check how the product chunk looks in bundle analyzer and determine its size before deprioritization:

Untitled

So at the moment, size of product chunk in Gzip is 68.7 KB

To deprioritize unnecessary components it is required to copy source Product Page component which is located in node_modules/@scandipwa/scandipwa/route/ProductPage and put into the projects src/route/ProductPage folder, it can be renamed to ProductPage.source.component.js file to avoid file duplicates. You would also need to copy project theme Product Page component which is located in node_modules/@scandiweb/<PROJECT-THEME>/src/route/ProductPage and put into the projects src/route/ProductPage folder, it can be renamed to ProductPage.theme.component.js file to avoid file duplicates This is needed to change existing inline imports of components to deprioritize them with lowPriorityLazy method.

Now, in ProductPage.source.component.js and ProductPage.theme.component.js change importing of components from inlining them to lowPriorityLazy .

For example, component ProductActions was imported into ProductPage.source.component.js using inline import:

import ProductActions from 'Component/ProductActions';

This import should be changed with deprioritization and webpack chunk name prefers to be product-misc:

export const ProductActions = lowPriorityLazy(() => import(
    /* webpackMode: "lazy", webpackChunkName: "product-misc" */
    'Component/ProductActions'
));

Now when a component is lazy loaded with low priority it is required to wrap it with Suspense component in extended file. Create ProductPage.component.js file where ProductPage class will be extended from previously copied source component and extending method which is responsible for rendering ProductActions component.

import { Suspense } from 'react';

/**
* Importing ProductPage class and ProductActions component
* from copy of local source file
*/
import {
	ProductPage as SourceProductPage,
  ProductActions,
} from './ProductPage.source.component';

export class ProductPage extends SourceProductPage {
	renderStickyBottomSection() {
        const {
            ...
        } = this.props;

        ...

				// Here ProductActions component is wrapped with Suspense
        return (
					...
          <Suspense fallback={ null } >
						<ProductActions
              ...
            />
					</Suspense>
					...
        );
    }
}

export default ProductPage;

This should be done for all components that are not considered to be in critical rendering path.

In result there should be a blank page, which is expected, since all of the components in ProductPage component are loaded with low priority.

After deprioritizing everything on a page, bundle analyzer shows less size code for product chunk.

Untitled