I Audited 100 Slow Shopify Stores. Here’s Why They Are Slow.
I ran WebPageTest audits on 100 Shopify stores over the past few months. Of the sites tested, the average mobile lighthouse performance score was 31 out of 100. These were not borderline cases; many of them were genuinely slow, especially on real‑world mobile conditions.
The same three issues plagued almost every site. These are clear problems with relatively simple fixes which can reduce load time by seconds, cut bounce rate, and increase profit—often without redesigning anything.
The Hero Images Aren't Prioritised
In 80 of 100 stores, the largest on-page element was an image—usually the hero banner—yet browsers weren't informed of its importance.
Browsers work out priorities based on what they discover and when they discover it. If your HTML, CSS, and JavaScript make it hard to see which image is the LCP, the browser can treat that hero like any other asset. Skipping fetchpriority and preload means the browser has to guess, which can cost precious time on slow devices and networks. Setting fetchpriority and preloading tells the browser: this image is critical—download it first.
80 stores were missing fetchpriority="high" on their LCP image entirely. 24 of those had gone one step further and added loading="lazy" or even a custom lazyload script, which actively tells the browser to deprioritise it. This is the opposite of what you want. The hero image is in the viewport on load. Lazy loading the LCP image frequently delays LCP by hundreds of milliseconds or more, and on already slow sites that can easily become 1–3 seconds.
You can usually fix this in a few minutes per template once you know where to look. Find the hero image in the theme’s section file and add two attributes:
<img
src="{{ hero_image_url }}"
fetchpriority="high"
loading="eager"
...>
For themes that use a lazy variable on image snippets, set it to false for the first image in the section. You can also preload the image in the <head>:
<link rel="preload"
as="image"
href="{{ hero_image_url }}"
fetchpriority="high">
On the very slow stores I tested, this single change often improved LCP by around 500ms up to a couple of seconds, because we were removing unnecessary delays from the most important image on the page.
The same stores that didn't prioritise loading the hero image on the home page almost always did the same thing with the main product image on the product page or category images if they had them, so every page was slowed down. Wherever your LCP image is, it should get first priority.
Third-Party Scripts Are Blocking
Every store shipped major unused or non‑critical JavaScript at page load, blocking the main thread and stalling interactivity. Unnecessary or badly timed code means wasted time and slower, jankier sites.
The usual suspects appeared over and over:
Google Tag Manager was on 64 stores, often transferring over 1 MB of scripts. Several stores loaded multiple duplicate gtag.js instances, each pulling 100–175 KiB. One store had four separate gtag scripts running simultaneously. You only need one, with multiple configuration calls if required.
Klaviyo ran on 60 stores, loading the full email and SMS SDK on every page—whether forms or pop-ups were used or not. Some stores suffered over 1,500ms of main-thread blocking just from this. On phone that doesn't make the site feel slow, it feels broken.
Chat widgets like Gorgias appeared on 20 stores, bringing the full chat UI framework every time—even though most visitors never open it. One store was spending ~650ms of main-thread time and ~600 KiB of transfer just on chat.
For most sites, none of these scripts need to load before the user can see and use the page. A better default is to defer them until the user interacts or a bit of time has passed:
function loadDeferred() {
[
'https://static.klaviyo.com/onsite/js/klaviyo.js',
// add other non-critical URLs here
].forEach(src => {
const s = document.createElement('script');
s.src = src;
s.async = true;
document.body.appendChild(s);
});
}
['scroll', 'mousemove', 'touchstart'].forEach(evt =>
window.addEventListener(evt, loadDeferred, { once: true })
);
// fallback: load after a delay for passive viewers
setTimeout(loadDeferred, 5000);
This approach works for Klaviyo, Gorgias, Hotjar, and most marketing tools. They’ll still load and function, but they no longer compete with core content and layout for CPU and bandwidth. If you have a strong business reason to track a subset of events immediately, keep those minimal and move everything else behind interaction or a delay.
For GTM, there are two big wins:
- Audit your container. Remove unused tags and triggers.
- Consolidate duplicate gtag.js instances into a single script with multiple
gtag('config', …)calls. Set non-essential tags to fire on DOM Ready or on specific events, not on every Page View.
When you combine this problem with the previous one, you end up with a serious issue: some stores were loading a full chatbot and several analytics SDKs before they’d finished loading the hero image. On some sites the first thing that loads on the screen is the chatbot! I'd love to know what they expect the customers to want to chat about.
Render-Blocking Resources in the Head
Every store tested had CSS or JavaScript files in the <head> that delayed the initial render. Some amount of render‑blocking CSS is unavoidable—you need styles to paint a page—but a lot of what I saw was simply not needed for that first screen.
The most common offenders:
Google Fonts loaded via fonts.googleapis.com were found in 32 stores as render-blocking resources in the head. One store was loading three separate font families this way. This adds extra DNS lookups, TLS handshakes, and a blocking stylesheet before text can render.
A better approach is to self-host the font files through Shopify’s CDN and preload them:
<link rel="preload"
href="{{ 'font-name.woff2' | asset_url }}"
as="font"
type="font/woff2"
crossorigin>
Combine this with font-display: swap in your CSS and you get much faster text rendering with fewer external dependencies.
Slider and carousel libraries like Owl Carousel, Slick, Flickity, and Swiper were used in 24 stores, loaded synchronously in the <head> as render-blocking scripts. These are typically 30–80 KiB gzipped, plus their accompanying CSS. Most of these sliders aren't even in the initial viewport. Instead:
- Load them only when their section scrolls into view (Intersection Observer).
- Or replace them entirely with CSS scroll-snap, as Shopify's own Dawn theme does for many carousels.
jQuery 3.5.1 was still blocking rendering in 27 stores, often due to a single app or legacy snippet. At minimum, you can usually defer it so it doesn’t block the first paint. Even better, if your theme and apps no longer depend on it, remove it entirely. Just be aware that if legacy code assumes jQuery is available synchronously, you may need to adjust that code before changing how jQuery loads.
Getting These Fixed
This handful of issues drives a large part of the performance gap on the slow Shopify stores I tested. Fix them first.
The top three fixes:
- Prioritise the hero / LCP image (no lazy, use fetchpriority, optional preload).
- Defer non-critical third-party scripts until interaction or a delay.
- Remove or restructure unnecessary render-blocking resources in the head (Google Fonts, sliders, legacy jQuery).
On the very slow sites in my sample, these changes alone would typically move mobile scores from the 20s or 30s into the 50s or 60s. That’s not a full optimisation, but it’s the difference between a site that feels broken and one that feels usable. Which is the difference between a life long customer for you or for your competitor.
On top of slow loading, many stores also had long running tasks that blocked the main thread—functions that should be broken up into smaller chunks. If you want to go deeper on that, I’ve written more about long blocking tasks here.
If you're struggling to make these fixes or want a more thorough performance tune up, send me a message or book a call here..