Core Web Vitals are Google’s metrics to measure a user’s page experience. One of the metrics is Largest Contentful Paint (LCP) which indicates the time it takes for the page’s main visible content to be shown to the user.

LCP is how Google now determines if a page is fast or slow. Google’s page experience algorithm wants an LCP score of no more than 2.5 seconds. Typically, LCP relates to the main image for a page.

Our Tag Rocket app gathers Core Web Vitals metrics which power our Core Web Vitals report. We used this information to work out how to dramatically improve the LCP score on our demo Cornerstone store via some alterations to the theme.

We made the following fixes on the base Cornerstone theme. They may need alteration if your theme has been modified.

Stop the product’s main image from lazy-loading

The most critical issue we saw was that the theme lazy-loaded the product page’s main image. And it was lazy-loading via JavaScript. This meant the image was not requested until the JavaScript was loaded and run. It was causing a significant delay in the image showing and a large LCP score.

Luckily this was an easy fix. Cornerstone uses a component to add images to the page (components/common/responsive-img), and it has an option to disable lazy loading. So we did that for the product’s main image:

templates->components->products->product-view.html

  
         <figure class="productView-image"
                data-image-gallery-main
                {{#if product.main_image}}
                data-zoom-image="{{getImageSrcset product.main_image (cdn theme_settings.default_image_product) 1x=theme_settings.zoom_size }}"
                {{/if}}
                >
            <div class="productView-img-container">
                {{!-- Remove the surrounding a-element if there is no main image. --}}
                {{#if product.main_image}}
                    <a href="{{getImageSrcset product.main_image (cdn theme_settings.default_image_product) 1x=theme_settings.zoom_size}}"
                        target="_blank"{{#if schema}} itemprop="image"{{/if}}>
                {{/if}}
                {{> components/common/responsive-img
                    image=product.main_image
                    class="productView-image--default"
                    fallback_size=theme_settings.product_size
                    lazyload='disabled'
                    default_image=theme_settings.default_image_product
                    otherAttributes="data-main-image"
                }}
                {{!-- Remove the surrounding a-element if there is no main image. --}}
                {{#if product.main_image}}
                    </a>
                {{/if}}
            </div>
        </figure>

Stop the category and brand pages top images from lazy-loading

A similar issue happens on the category and brand pages. In this case, the first row of product images on the page often causes the LCP. On mobile devices, it is typically only the first product image visible as the page loads.

Lazy-loading mechanisms aim to load images before they come into view as the user scrolls. From some tests, I decided that lazy-loading the first few rows of product images was of little value as the lazy-loading mechanism would load them straight away.

We needed to add some code to disable lazy loading for the first set of product images on the page. Here I’ve chosen to disable it for the first 6, which are two rows on a desktop with my theme.

templates->components->products->card.html

        <a href="{{url}}"
           class="card-figure__link"
           aria-label="{{name}},{{> components/products/product-aria-label}}"
           {{#if settings.data_tag_enabled}} data-event-type="product-click" {{/if}}
        >
            <div class="card-img-container">
                {{#and (if position '<=' 6)}}
                {{> components/common/responsive-img
                    image=image
                    class="card-image"
                    fallback_size=theme_settings.productgallery_size
                    lazyload='disabled'
                    default_image=theme_settings.default_image_product
                }}
                {{else}}
                {{> components/common/responsive-img
                    image=image
                    class="card-image"
                    fallback_size=theme_settings.productgallery_size
                    lazyload=theme_settings.lazyload_mode
                    default_image=theme_settings.default_image_product
                }}
                {{/and}}
            </div>
        </a>

This code will work on all pages using product cards. The home page contains multiple sets of product cards lower down on the page, like featured/new/popular products. You may want to enhance it so that it does not disable lazy-loading in those cases.

Preload images

The above changes did not rock the boat much with regard to LCP, but they did open the option to preload the images. The combination means the LCP images start loading immediately and show as soon as the HTML is rendered.

At this time, I’d only implement this if you know what you are doing and you do detailed tests.

We also found that the home page’s carousel was the LCP. The first image already had lazy-loading disables, so we only needed to preload it to reduce the LCP score.

layout->base.html

Place this code early in the head section.

        {{#if product.main_image}}
        <link rel="preload" imagesrcset="{{getImageSrcset product.main_image use_default_sizes=true}}" as="image"  imagesizes="{{concat (first (split theme_settings.product_size 'x')) 'px'}}">
        {{/if}}
        {{#if category.products.[0].image}}
        <link rel="preload" imagesrcset="{{getImageSrcset category.products.[0].image use_default_sizes=true}}" as="image" imagesizes="320px">
        {{/if}}
        {{#if brand.products.[0].image}}
        <link rel="preload" imagesrcset="{{getImageSrcset brand.products.[0].image use_default_sizes=true}}" as="image" imagesizes="320px">
        {{/if}}
        {{#if carousel.slides.[0].stencil_image}}
        <link rel="preload" imagesrcset="{{getImageSrcset carousel.slides.[0].stencil_image use_default_sizes=true}}" as="image" imagesizes="(max-width: 640px) 640px, (max-width: 1280px) 1280px, (max-width: 1920px) 1920px, 2560px">
        {{/if}}

Note: for preloading to work, it has to use the same image URL and srcset/sizes (if present) as the image causing the LCP issue. If not, the preload is just wasting time and bandwidth. It can be tricky to determine what values the sizes use as the theme uses the lazysizes script, which calculates the sizes on the fly. For example, the product images on a category/brand page grow and shrink as the number of columns changes. So this is not a perfect solution.

A bug in the theme meant the sizes attributes were not set when lazy-loading was disabled. I implemented a variation of this pull request to enable the setting of sizes when lazy-load is disabled. Hopefully, soon a clean solution will be available.

It could also be a good move to preload important images defined in CSS as they are discovered late and have low importance by default.

Fetch Priority

fetchpriority is a new attribute (Only in Chrome at this time) that can be applied to img tags. Setting the priority to high is an equivalent and neater way to have it preload. At this time, I would implement both fetchpriority and preloads for your main images. You can also apply fetchpriority on the preload to help it load earlier.

<link rel="preload" href="..." as="image" fetchpriority="high"/>

<img src="..." fetchpriority="high"/>

Conclusion

I tested the demo account on my machine and saw LCP reductions in the 100s of milliseconds up to half a second.

We implemented similar changes for one client (They had a customised theme) and saw their LCP score move from “needs improvement” to “good”. We also fixed their Cumulative Layout Shift (CLS) issues meaning they now pass all metrics and are eligible for the page experience ranking boost.

Check out our How To Make BigCommerce Fast article to go more in-depth into speeding up a BigCommerce store.

Remember that Tag Rocket can gather your real Core Web Vitals metrics from your visitors and report them in detail. That is how we discovered these issues.

I’d love to hear how well it works for you.