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
- Step 2: Reducing Load Delay
- Step 3: Reducing Render Delay
- Step 4: Reducing server-side render delay
- Step 5: High level summary
- Step 6: Recommended resources
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.

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:
- Our end goal: get good LCP numbers for our production website using Google PageSpeed. PageSpeed is a free, publicly available performance audit tool.
- 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. - 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.

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.

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

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.

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.

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.

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:

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


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.

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"
andrel="preload"
)
- is discoverable (e.g.: preloaded in
- 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.