How to Improve Shopify Store Speed by Fixing LCP (Largest Contentful Paint)

How to Improve Shopify Store Speed by Fixing LCP (Largest Contentful Paint)

I have created my first Shopify theme, Coffee World. I took this opportunity to learn about increasing web performance, implement my learnings and document the process.

In this article, I’ll walk you through practical examples and show you how you can improve your Shopify store’s performance. We’ll do it by improving a very important part of your website: the first most visible and most resource demanding visual element of your website, also known as your LCP.

Table of contents

Step 1: Understanding LCP

1.1: Why web performance is important

Web performance is a term used to measure a website’s speed, stability and reliability. The better performance numbers you get, the smoother the user experience is while browsing your website. Increasing your web performance numbers have two major impacts:

  • Better user experience resulting in more conversions, fewer bounces and happier customers
  • Better performance resulting in better SEO and better discoverability

On the other hand, bad web performance number might cause your visitors headaches and frustrations. If your website is slow in ideal conditions - browsing with fast internet connection on strong laptop -, imagine what happens in bad conditions - with slow internet on a slow device.

Good web performance numbers will attract not only customers but algorithms too; search engines rank fast websites better than slow websites.

The good news is that measuring performance is easy. We have the tools available for free, telling us more or less exactly what we need to do to create a speedier, more performant website.

1.2: What is LCP?

LCP is part of the Core Web Vitals, which is basically a set of “most important rules” defined by Google when it comes to web performance. Chrome has built-in tools to measure Core Web Vitals and many pages and platforms (including Shopify) use Core Web Vitals to evaluate your website.

LCP is the largest visible content element (image or text block) in the viewport rendered during page load. Usually it’s an image: either the feature image of a blog post or the hero image of your website. It’s usually the most visually prominent part of the viewport, therefore optimizing it increases UX dramatically.

LCP is often a hero image or an image of a featured article

1.3: How to evaluate LCP?

We measure LCP performance against time. The faster the better. Here are the evaluation ranks according to Google:

LCP scoring on mobile

LCP time (in seconds) Color-coding
0-2.5 🟢 Fast
2.5-4 🟠 Moderate
Over 4 🔴 Slow

LCP Scoring on desktop

LCP time (in seconds) Color-coding
0-1.2 🟢 Fast
1.2-2.4 🟠 Moderate
Over 2.4 🔴 Slow

Remember: Mobile users often experience much slower load/render times, so prioritize testing on mobile throttled conditions first.

1.4: LCP subparts

The first thing we need to understand about LCP is that it has 4 subparts (phases) that follow each other. Identifying which supbart takes longer than expected will help us to know what to improve.

LCP subpart Description Optimal % of LCP
Time to first byte (TTFB) The time from when the user initiates loading the page until the browser receives the first byte of the HTML document response. ~40%
Resource Load Time delay
( or simply “load delay”)
The time between TTFB and when the browser starts loading the LCP resource.
<10%
Resource Load time The duration of time it takes to load the LCP resource itself. ~40%
Element Render Time Delay
(or simply “render delay”)
The time between when the LCP resource finishes loading and until the LCP element is fully rendered <10%

Google suggests to first work on improving Resource Load Time Delay (Load Delay) and Element Render Time Delay (Render Delay). We have most control over these from the frontend (e.g.: from our Shopify theme) and Google lab data also shows this is where most websites bleed.

1.5: Performance improvement strategy

Let’s define what we want to achieve:

  1. Our end goal: get good LCP numbers for our production website using Google PageSpeed. PageSpeed is a free, publicly available performance audit tool.
  2. Our intermediate goal: get good LCP numbers on localhost using Lighthouse. Lighthouse is a performance audit tool built-in to Chrome, giving us high level insights about performance.
  3. Our first goal: get good LCP numbers on localhost using Chrome Dev Tools Performance tab. Performane tab is a low level performance audit tool, giving is all the nitty-gritty details to debug and improve.

This step-by-step approach will help us solve issues locally with fast changes, and then scale our solution up to production where anyone can see it.

1.6: Technical tips before you begin

One last section before we dive in with some technical tips:

  • Use a separate browser without accounts and plugins, like Chrome Dev so your personal customizations won’t affect performance measuring.
  • Toggle throttling when using Performance tab, so your results will have a closer match to Lightohuse results:
    • CPU: 4x slowdown - recommended
    • Network: Slow 4G
  • Run tests for both mobile and desktop views 🙂
  • Identify repaints and render bottlenecks when browsing the performance flamechart: Chrome Devtools > Render > Check “Paint flashing” and “Layout Shift Regions

Now that we’re prepared, let’s dive into the improvements. Here’s how to improve LCP.

Step 2: Reducing Load Delay

The goal of improving load delay is to ensure the LCP resource starts loading as early as possible. We can achieve this by eliminating or deprioritizing other, unnecessary resource loads. Our LCP might not get loaded in time because of two reasons:

  • Prioritization: Higher priority assets get loaded first
  • Discoverability: Something blocks the load (e.g.: script) and/or the LCP is not yet discovered by the browser.

2.1: Solving prioritization issue

To see the priority of your LCP resource go to Performance tab, click on LCP by phase to highlight your LCP element in the waterfall chart (light blue stroke). Hover with mouse too see it’s priority.

Use Performance tab to see LCP image resource load priority

To increase the priority of a resource we can use the rel and fetchpriority attributes.

<link fetchpriority="high" rel="preload" href="/images/large/london.jpg" as="image">

Use rel and fetchpriority to control resource load priority

LCP mismatch

You might load multiple versions of your image with multiple sizes. This is common in Shopify. You can see this phenomenon by clicking on LCP by phase box and discovering that not the expected version gets highlighted, but a bigger one. This will produce worse LCP numbers.

If that’s the case, make sure you are loading only the right version of your image. To achieve that you can use the picture HTML element.

<picture>
  <source media="(min-width:650px)" srcset="img_pink_flowers.jpg">
  <source media="(min-width:465px)" srcset="img_white_flower.jpg">
  <img src="img_orange_flowers.jpg" alt="Flowers" style="width:auto;">
</picture>

The picture HTML element can be used to use various versions of your image

Learn more: https://www.w3schools.com/tags/tag_picture.asp

Resource priority order

Browsers use the rel and fetchpriority attributes to evaluate priority, and set up the following priority order:

Element Priority
element without rel="preload” ⚪ Lowest (no priority)
element with <link rel="preload" fetchpriority="low”> 🔵 Low
element with <link rel="preload"> 🟡 Medium
element with <link rel="preload" fetchpriority="high”> 🔴 High

If our resource delay score is till low despite that we’ve added these necessary attributes to our LCP image, and removed them from other elements that might compete for prioritization… we might have a discoverability issue.

2.2: Solving discoverability issue

Our LCP image can have high priority definitions, if the browser discover the element too late, it will start loading it too late. Go to Network, find your LCP resource request, see Timing, see Queued and Started. If it gets queued over 800ms, browser learned about the resource way too late.

Use Network Timing to see if your LCP load was delayed

This might be often the case with Shopify themes, because your LCP image might be within a section (e.g.: Hero section) which gets loaded later then other resources.

Index.html <head> is the best for discoverability

Unfortunately, this is a common issue with Shopify themes, that derives from the structure Shopify themes are built (e.g.: Dawn). One workaround is to add your image setting on a global level (settings_schema.json). This way you can use the theme settings in directly your index.html header (theme.liquid). Putting your LCP resource element directly in your index.html <head> is ensuring that the browser discovers the element as soon as possible.

For example, if you are trying to display a product image in your hero section, you might add a product typed theme setting in your settings_scheme.json and use the setting in your index.html template (theme.liquid).

{
  "name": "Hero Settings",
  "settings": [
    {
      "type": "product",
      "id": "hero_product",
      "label": "Hero Product"
    }
  ]
},

settings_scheme.json

Then reference it in your theme.liquid:

{% assign hero_product = all_products[settings.hero_product] %}
    {% if hero_product.featured_image %}
      <link
        rel="preload"
        as="image"
        href="{{ hero_product.featured_image | image_url: width: 300 }}"
        media="(max-width: 749px)"
        fetchpriority="high"
      >
      <link
        rel="preload"
        as="image"
        href="{{ hero_product.featured_image | image_url: width: 800 }}"
        media="(min-width: 750px)"
        fetchpriority="high"
      >
{% endif %}

theme.liquid

This approach comes with a UI trade-off. Now the admin has to set the content from in the Theme Settings, not from your Hero section. However, this way we can directly reference this content in index.html header - which is the fastest way to tell the browser to start downloading something. By adding preload and fetchpriority attributes too, it will be prioritized over other items too.

Note that this approach will need more validation, e.g.: you don’t want to load the product image on every page probably, only where the hero section is present. On other pages, you might want to load other LCP images.

To learn more about optimizing load delay, refer to this article by Google.

Step 3: Reducing Render Delay

The second LCP phase we’ll improve is Element Render Time Delay (or simply Render Delay). This is time spent between when your LCP resource was loaded and rendered. Here our goal is to ensure that the LCP element can render immediately after its resource has finished loading - no matter when that happens.

Of course, usually this is not the case, because there are other factors in play. Usually the culprits are:

  • Stylesheets, synchronous scripts in the <head>
  • LCP is loaded but not added to the document (because a JavaScript adds it dynamically)
  • Other Long Tasks
In this simple example a large stylesheet is blocking the LCP to render

The first step when it comes to render delay is to identify what blocks the render. To do so, go to Performance and in the Main Thread flamechart, find the Element Render Delay range, and within it, look for a green Paint block with Layer root of your LCP image. Everything before that but within the Element render delay range blocks the render.

To identify what blocks the render, first find the Paint block of your LCP resource in your Main thread

Once we know where the LCP gets painted, we can start learning more about the tasks that block it. Go through tasks (red blocks) one-by-one, and start with the biggest tasks.

Step 3.1: Fixing HTML element order

One of the causes of render delay is render prioritization. For example, in the case of developing Coffee World, I’ve noticed that drawing the background image of my hero section actually blocked, drawing the LCP image.

Non-critical resource load (hero-background) loads before my LCP resource

The simplest technique, is to simply bring up your preferred element in HTML. This might be the solution if both your elements are positioned absolute and/or if both elemenets are in the same section.

<div class="relative-hero">
  <!-- LCP element first --> 
  <img class="absolute-lcp-first" src="..." />

  <!-- Background element second -->
  <div class="absolute-background-second"></div>
</div>

HTML element order matters in render prioritization

3.2: Setting content visibility

If you are dealing with elements across multiple sections or not within the same parent, you can use CSS tricks to set prioritization. This might be the case for example if your navigation bar gets painted before your LCP image, which makes sense, because your navbar is usually comes first in your HTML.

.navigation-header { 
  content-visibility: auto;  /* Method A: most effective */
  contain-intrinsic-size: 100% 80px; /* Expected header height */
  z-index: 0; /* Method B: good fallback  */
}

.lcp-element { 
  content-visibility: visible; /* Force immediate rendering */
  z-index: 1; 
}

Use content-visibility CSS rule to prioritize render

In order for this to work, your CSS file containing these rules should be loaded and parsed too.

3.3: Speeding up Long Tasks

A task is considered Long Task if it’s above 50ms according to Google. We can improve the speed of our task by shortening our “Recalculate style” and “Layout” blocks. In my case, simply rendering the LCP image was itself a Long Task.

Rendering the LCP resource is a long task

Low coverage

The most common cause of expensive style recalculation is bad CSS coverage. To see if you have bad coverage go to Chrome Dev Tools and see the Coverage tab.

Look at all the CSS rules my homepage does not use:

78.9% of my base.css is unused, resulting in a 21.1% coverage

Shopify Dawn theme uses a huge base.css, which preloads a sea of CSS rules. This is because many section styles are dumped into this file, which is then evaluated on ALL pages, even when the section is not present. This is suboptimal, and all the CSS rules should be moved to the relevant template files, and the unused should be of course deleted.

Tip to test: A quick way to see performance impact of having unused CSS is to temporarily remove all unused CSS using UnCSS Online! for a given HTML file and then running the audit again. We’ll see that our LCP score is increased tremendously. This technique is only for problem validation of course. For a quick and dirty improvement we can load critical css for our homepage and load the rest for all others. This solution is for demonstrational purposes only.

{%- if request.page_type == 'index' -%}
      {{ 'base--home-critical.css' | asset_url | stylesheet_tag }}
    {%- else -%}
      {{ 'base.css' | asset_url | stylesheet_tag }}
{%- endif -%}

Separate base.css and base--critical.css for testing purposes

Slow LCP render delay before removing unused CSS
Fast LCP render delay after removing unused CSS

CSS selector complexity

The second most common problem is too complex CSS selectors, which cause the browser to render slowly. Complex selectors must be updated to simpler, performance-light selectors. For example, instead of .hero:not(.active) -> hero--inactive.

Selector Type Example Why It's Expensive
Deep descendant selectors .hero div ul li span Browser must match deep trees
Universal selectors inside .hero * Matches all children
Attribute selectors .hero input[type="text"] Slower than class/id selectors
Complex combinators .hero > .container + .title Costly traversal logic
Negations or pseudo-classes .hero:not(.active) Adds complexity to evaluation
/* Bad: Complex selectors cause slow recalculation */
.shopify-section .hero-wrapper .container .row .col .hero-image {
  /* styles */
}

/* Good: Simple, specific selectors */
.hero-image {
  /* styles */
}

Complex CSS selectors are expensive

We can identify complex selectors using the Performance tab. Make sure Enable CSS selector stats is checked in Chrome Performance tab. Then identify your task, click on the purple “Recalculate style” block, then click “Selector stats” tab. Organise your selectors by Elapsed (ms), and voilá: here’s a list of your most performance-costly CSS selectors.

Enable CSS selector stats to see your expensive CSS selectors

You can also audit selectors manually for a given section. Use the following snippiet in your console to print all CSS selectors that are related to your inspected element (e.g.: .hero).

// Run in console to find expensive selectors
Array.from(document.styleSheets)
  .flatMap(sheet => Array.from(sheet.cssRules))
  .filter(rule => rule.selectorText && rule.selectorText.includes('.hero'))
  .forEach(rule => console.log(rule.selectorText));

Script to print CSS selectors for a given context

This will print out a list of CSS selectors used in your document. Look for performance-heavy selectors (see table above).

Other tips

If you want to dig deeper and reduce Render Delay even furthere here are some tips on how to fine-tune your CSS:

Improving for Style recalculation by using CSS containment

.hero-section {
  contain: style layout; /* Isolate style calculations */
}

.hero-image {
  contain: layout style paint;
}

Use the contain CSS rule to improve Style recalculation

Improving for Style recalculation by minimizing CSS rules

/* Bad: Too many properties trigger recalculation */
.hero-image {
  position: relative;
  transform: translateX(0) translateY(0) scale(1) rotate(0deg);
  filter: brightness(1) contrast(1) saturate(1);
  box-shadow: 0 0 0 rgba(0,0,0,0);
  /* ... 20+ more properties */
}

/* Good: Only essential properties */
.hero-image {
  display: block;
  width: 100%;
  height: auto;
}

Less CSS rules results in a faster document read and render

Improving Layout speed by simplifying DOM structure

<!-- Bad: Deep nesting causes slow layout -->
<div class="hero-section">
  <div class="container">
    <div class="row">
      <div class="col-12">
        <div class="hero-wrapper">
          <div class="image-container">
            <img class="hero-image" />
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Good: Flat structure -->
<section class="hero-section">
  <img class="hero-image" />
</section>

Simpler HTML structure results in faster document read and render

Improving Layout speed by using modern CSS layout (grid or flexbox)

/* Replace complex float/positioning with modern CSS */
.hero-section {
  display: grid;
  place-items: center; /* Instead of complex centering */
}

/* Or use flexbox */
.hero-section {
  display: flex;
  align-items: center;
  justify-content: center;
}

Grid and flexbox is faster than old float positioning

Improving Layout speed by avoiding layout-triggering properties

css/* Bad: Forces layout recalculation */
.hero-image {
  width: calc(100vw - 40px);
  height: calc(100vh - 100px);
  margin-left: calc(50% - 200px);
}

/* Good: Static dimensions */
.hero-image {
  width: 100%;
  max-width: 800px;
  height: auto;
}

Remove layout triggering properties to improve Layout speed

Improving Layout speed by using transform instead of Layout Properties

/* Bad: Triggers layout */
.hero-image {
  margin-left: 50px;
  top: 100px;
}

/* Good: Uses composite layer */
.hero-image {
  transform: translateX(50px) translateY(100px);
}

Transform is faster than layout properties

Step 4: Reducing server-side render delay

Shopify's Liquid templates are compiled and rendered server-side before the resulting HTML is sent to the user's browser. This means that by writing better Liquid syntax we can reduce the time the server needs to render our HTML.

In order to keep this article concise, I will not explain this in detail, but as a general rule here are sime guidelines:

  • Avoid deep nesting of {% for %} loops.
  • Use {% unless %} and {% else %} wisely to avoid unnecessary checks
  • Cache large data sets (use pagination or limit filters)
  • Reuse computed variables instead of repeating expensive filters
  • Minimize use of all_products where possible (limited and performance-heavy)

Step 5: High level summary

To recap everything we've learned in this post, here's a high level summary:

  • Web performance increases UX and SEO
  • To improve web performance we can rely on Core Web Vitals, e.g.: LCP
  • LCP is the largest visible content element in the viewport during page load
  • LCP has 4 subparts and we should primarily focus on 2: load delay and render delay
  • To improve load delay, make sure our element
    • is discoverable (e.g.: preloaded in <head>)
    • has high priority (fetchpriority="high" and rel="preload")
  • To improve render delay,
    • make sure our element is placed before other elements in HTML
    • remove unused CSS
    • reduce CSS complexity by replacing performance-heavy selectors with simple ones
    • using performance-light CSS rules
    • deferring non-critical CSS and JS
  • We can improve server-side render delay for Shopify by writing better Liquid

I hope you picked up something new reading this article. Remember: fast performance means better UX — and better UX means more trust, happier users, and better conversions.

Gábor Pintér

Written by Gábor Pintér

Hi, my name is Gábor Pintér. I am a Shopify expert with over 10 years of experience in web development. I am using my technical expertise to help business owners to make their Shopify stores the best it can be.

Get Free Consultation →