How to Speed Up a Shopify Store: 15 Fixes That Move Metrics
How to speed up a Shopify store: on 23 stores, apps and third-party tags were 75% of product-page JavaScript. 15 fixes, each mapped to LCP, TBT or CLS.
To speed up a Shopify store, start with apps and third-party tags, not the theme. On the product pages we measured, they were the bulk of the JavaScript, and blocking them cut median Largest Contentful Paint by 2.7 seconds and Total Blocking Time by 4.6 seconds on a mid-range phone. Then fix the hero image, fonts and theme features. Each change below is mapped to the metric it moves, so you can measure it before and after.
On 10 October 2026 we loaded the product pages of 23 Shopify stores, found in a random sample of the Tranco top 200,000, on a throttled mobile profile, twice each: as shipped, and with app embeds and third-party scripts blocked. The median product page shipped 2.8 MB of JavaScript, of which 2.1 MB (75%) came from third-party hosts. The theme's own JavaScript was a median of 339 KB. Median lab LCP was 7.5 s as shipped and 4.7 s with apps blocked. Median TBT fell from 5.4 s to 1.4 s.
The blocked run is a ceiling, not a target. Some of those scripts are reviews, subscriptions and search that the store needs. But it shows where the time goes, and it's rarely the theme.
What We Measured
| Item | Detail |
|---|---|
| Stores | 64 Shopify stores identified in a random sample of 6,000 Tranco 94GG2 domains (ranks 1,001–200,000, seed 20261010); the first 30 product pages measured, 23 valid (one timed out, six failed in our measurement script) |
| Page | One product page per store (from the store's first collection) |
| Profile | 412×823 mobile viewport, mobile Chrome user agent, 150 ms RTT, 1.6 Mbps down / 750 kbps up, 4× CPU slowdown; one load per variant |
| Variants | As shipped, and apps blocked: requests to cdn.shopify.com/extensions/… (theme app embeds) and to every non-Shopify third-party host aborted |
| JavaScript bytes | Encoded transfer size of each script request from Chrome DevTools Protocol network events, grouped by origin |
| Product page, median of 23 stores | As shipped | Apps & third-party blocked |
|---|---|---|
| Largest Contentful Paint (lab) | 7.5 s | 4.7 s |
| Total Blocking Time (lab) | 5.4 s | 1.4 s |
| Stores with lab LCP ≤ 2.5 s | 2 | 3 |
| Per-store LCP saving (median) | 2.7 s | |
| Per-store TBT saving (median) | 4.6 s | |
| Stores where blocking saved > 1 s of LCP | 20 of 23 |
| JavaScript by origin (median per product page) | KB |
|---|---|
| Third-party apps & tags (any non-Shopify host) | 2,089 |
| Theme (store domain + theme files on Shopify's CDN) | 339 |
App embeds (theme app extensions on cdn.shopify.com/extensions/) | 53 |
| Shopify platform (checkout, analytics, Shop Pay) | 42 |
| Total | 2,820 |
Pages loaded a median of 129 scripts from 15 different third-party hosts, up to 32. The most common hosts were Google Tag Manager (21 of 23 stores), the Meta pixel (17), jsDelivr (13: libraries loaded from a public CDN), Google Ads (11), Klaviyo (11), Microsoft Clarity (10), cdnjs (7) and Bing (7). Review, chat and checkout widgets (Yotpo, Gorgias, GoKwik and others) appeared on several stores each.
Even fully blocked, only 3 of 23 stores reached a 2.5 s lab LCP. These lab numbers are deliberately harsh: Google's Core Web Vitals assessment uses real-user field data, which is usually faster. Images, fonts and server time still matter after the apps. That's what fixes 6–15 cover.
Fixes for Apps and Third-Party Scripts (the Biggest Lever)
1. Audit apps against revenue, and uninstall what doesn't earn
List every app and ask what it contributes to revenue or operations. Uninstall the rest. Moves: TBT, INP, LCP, total bytes.
2. Remove leftover code after uninstalling
Older apps inject snippets into theme files that stay behind after uninstall. Search the theme code for the app's name and for {% render 'app-name' %} / {% include %} references, and check theme.liquid for hard-coded <script> tags. Moves: TBT, bytes.
3. Prefer apps built on theme app extensions
App embeds served from cdn.shopify.com/extensions/ were the smallest app category in our data (median 53 KB) and can be switched off per theme in the theme editor's App embeds panel. Script-tag apps that load from their own servers made up most of the 2.1 MB. Moves: TBT, bytes.
4. Load chat, reviews and pop-ups on interaction or idle
A chat widget isn't needed for the first paint. Load it after a click on a placeholder button, or after the page is idle:
<!-- ✅ Chat loads only when the visitor asks for it -->
<button id="chat-facade" aria-label="Open chat">💬</button>
<script>
document.getElementById('chat-facade').addEventListener('click', () => {
const s = document.createElement('script');
s.src = 'https://chat-vendor.example/widget.js'; s.async = true;
document.head.appendChild(s);
}, { once: true });
</script>For reviews below the fold, inject the widget when its container scrolls into view (IntersectionObserver). Moves: TBT, INP, LCP.
5. Consolidate tracking pixels
Google Tag Manager, the Meta pixel, Google Ads, Klaviyo, Clarity, Bing and TikTok were all common, often several on one store. Move marketing pixels into Shopify's Customer events (the sandboxed web pixel manager) or a single tag manager container. Remove pixels for channels you no longer advertise on. Keep consent in mind: our cookie compliance scanner guide covers firing them only after opt-in. Moves: TBT, INP.
Fixes for the Theme and Assets
6. Stop loading libraries from public CDNs
13 of 23 stores loaded scripts from jsDelivr and 7 from cdnjs, often jQuery or slider libraries pulled in by apps on top of the theme's own copy. Remove duplicates and let one copy load (or none: modern themes don't need jQuery). Moves: TBT, bytes.
7. Size the hero image properly and let it load first
{% comment %} ✅ Responsive hero image, eager, high priority, explicit dimensions {% endcomment %}
{{ product.featured_image | image_url: width: 1200 | image_tag:
widths: '400, 600, 800, 1000, 1200',
sizes: '(min-width: 990px) 50vw, 100vw',
loading: 'eager', fetchpriority: 'high' }}Never loading="lazy" the LCP image. Shopify's image_url serves the image from its CDN (in WebP where the browser supports it), so the remaining job is not to request a 3,000 px image for a 400 px slot. See Google's Optimize LCP. Moves: LCP.
8. Lazy-load everything below the fold
Gallery thumbnails, recommendation carousels and footer images should use loading="lazy", and so should below-the-fold sections that pull in their own scripts. Moves: LCP (less contention), bytes.
9. Cut font weight
Use the theme's system-font option, or one family with two weights, font-display: swap, and a preload for the main weight. Icon fonts (Font Awesome loaded from a kit, as on the store below) can usually be replaced with inline SVG icons. Moves: LCP, CLS.
10. Turn off theme features you don't use
Quick view, predictive search, animations, mega-menus with product images and countdown timers each add JavaScript and DOM. Disable them in theme settings, or remove the sections. Moves: TBT, INP.
11. Choose a lightweight theme when you redesign
The theme's own JavaScript (median 339 KB) wasn't the main problem in our data, but it varies by theme. Shopify's theme performance best practices list what to look for: minimal JavaScript, native lazy loading and responsive images. Moves: TBT, LCP.
12. Replace embedded videos with facades
A YouTube or Vimeo iframe loads hundreds of KB of player code. Show a thumbnail and load the player on click. Moves: TBT, LCP.
13. Reserve space for widgets
Review stars, "buy now, pay later" messages, trust badges and app banners that load late shift the layout. Give their containers a min-height. 4 of 23 product pages had a lab CLS above 0.1. Moves: CLS.
14. Watch variant-heavy product pages
Products with many variants can render large option tables and image sets. Load only the selected variant's images first, and fetch the rest on demand. Moves: LCP, bytes.
15. Measure on a throttled phone, before and after each change
Test the product template, not just the homepage. Use a throttled mobile profile and change one thing at a time, so you know which fix earned which gain. The script below and Chrome DevTools' Coverage panel are enough to start.
Measure What Your Apps Cost: Script
This script loads a product page on the same throttled profile as our study, splits JavaScript by origin and reloads with app embeds and third-party scripts blocked:
#!/usr/bin/env python3
"""shopify_app_cost.py: how much JavaScript do a Shopify store's apps add, and what would blocking them gain?
Usage:
pip install playwright && playwright install chromium
python3 shopify_app_cost.py https://store.example.com/products/some-product [--runs 2]
Loads the page on a throttled mid-range-phone profile (412x823, 150 ms RTT, 1.6 Mbps, 4x CPU) and
splits JavaScript bytes by origin: theme, Shopify platform, app embeds (cdn.shopify.com/extensions)
and third-party apps & tags. Then reloads with app embeds and third-party scripts blocked and
reports the median LCP, TBT and script bytes of each variant. The blocked run shows the ceiling of
what pruning apps could buy; some apps (reviews, subscriptions) are features you will keep.
"""
import argparse, asyncio, statistics
from urllib.parse import urlsplit
from playwright.async_api import async_playwright
UA = ("Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36")
INIT = r"""
window.__lcp = 0; window.__fcp = 0; window.__lt = [];
new PerformanceObserver(l => { for (const e of l.getEntries()) window.__lcp = e.startTime; }).observe({type: 'largest-contentful-paint', buffered: true});
new PerformanceObserver(l => { for (const e of l.getEntries()) if (e.name === 'first-contentful-paint') window.__fcp = e.startTime; }).observe({type: 'paint', buffered: true});
new PerformanceObserver(l => { for (const e of l.getEntries()) window.__lt.push([e.startTime, e.duration]); }).observe({type: 'longtask', buffered: true});
"""
COLLECT = """() => ({LCP: window.__lcp, TBT: window.__lt.reduce((a, [s, d]) => a + (s + d > window.__fcp && d > 50 ? d - 50 : 0), 0)})"""
def origin(url, store):
h, p = urlsplit(url).hostname or "", urlsplit(url).path
if h == store or h.endswith("." + store.removeprefix("www.")):
return "theme / store domain"
if h.endswith("cdn.shopify.com") or h.endswith("shopifycdn.com"):
return "app embeds (Shopify CDN)" if "/extensions/" in p else (
"Shopify platform" if "/shopifycloud/" in p else "theme / store domain")
if h.endswith("shopify.com") or h.endswith("shop.app") or h.endswith("shopifysvc.com"):
return "Shopify platform"
return "third-party apps & tags"
def blockable(url, store):
return origin(url, store) in ("app embeds (Shopify CDN)", "third-party apps & tags")
async def load(browser, url, block):
store = urlsplit(url).hostname
ctx = await browser.new_context(user_agent=UA, viewport={"width": 412, "height": 823},
device_scale_factor=1.75, is_mobile=True, has_touch=True)
await ctx.add_init_script(INIT)
page = await ctx.new_page()
cdp = await ctx.new_cdp_session(page)
reqs, size = {}, {}
cdp.on("Network.requestWillBeSent", lambda e: reqs.__setitem__(e["requestId"], (e["request"]["url"], e.get("type"))))
cdp.on("Network.loadingFinished", lambda e: size.__setitem__(e["requestId"], e.get("encodedDataLength", 0)))
await cdp.send("Network.enable")
await cdp.send("Network.emulateNetworkConditions", {"offline": False, "latency": 150,
"downloadThroughput": 1.6 * 1024 * 1024 / 8, "uploadThroughput": 750 * 1024 / 8})
await cdp.send("Emulation.setCPUThrottlingRate", {"rate": 4})
if block:
await page.route("**/*", lambda r: r.abort() if blockable(r.request.url, store) else r.continue_())
await page.goto(url, wait_until="load", timeout=90000)
await page.wait_for_timeout(5000)
m = await page.evaluate(COLLECT)
js, hosts = {}, set()
for rid, (u, t) in reqs.items():
if t == "Script":
k = origin(u, store)
js[k] = js.get(k, 0) + size.get(rid, 0)
if k == "third-party apps & tags":
hosts.add(urlsplit(u).hostname)
await ctx.close()
return m, js, hosts
async def main(url, runs):
async with async_playwright() as pw:
browser = await pw.chromium.launch()
normal = [await load(browser, url, False) for _ in range(runs)]
blocked = [await load(browser, url, True) for _ in range(runs)]
await browser.close()
med = lambda rs, k: statistics.median(r[0][k] for r in rs)
print(f"\n{url}\nJavaScript transferred (first normal run):")
js = normal[0][1]
for k in ("theme / store domain", "Shopify platform", "app embeds (Shopify CDN)", "third-party apps & tags"):
print(f" {k:28s} {js.get(k, 0) / 1024:8.0f} KB")
print(f" third-party script hosts: {', '.join(sorted(normal[0][2])) or 'none'}")
print(f"\n{'':22s}{'LCP':>9s}{'TBT':>9s}{'JS KB':>9s}")
for label, rs in (("as shipped", normal), ("apps/3p blocked", blocked)):
kb = statistics.median(sum(r[1].values()) for r in rs) / 1024
print(f" {label:20s}{med(rs, 'LCP') / 1000:8.1f}s{med(rs, 'TBT') / 1000:8.1f}s{kb:9.0f}")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("url")
ap.add_argument("--runs", type=int, default=2)
a = ap.parse_args()
asyncio.run(main(a.url, a.runs))Real output for a plant nursery's product page (10 October 2026, domain replaced):
https://plant-nursery.example/products/abutilon-twister
JavaScript transferred (first normal run):
theme / store domain 214 KB
Shopify platform 34 KB
app embeds (Shopify CDN) 102 KB
third-party apps & tags 2891 KB
third-party script hosts: ajax.aspnetcdn.com, ajax.googleapis.com, assets.gorgias.chat, capi-automation.s3.us-east-2.amazonaws.com, cdn.jsdelivr.net, cdn.rebuyengine.com, cdn.recovermycart.com, cdnjs.cloudflare.com, config.gorgias.chat, connect.facebook.net, kit.fontawesome.com, media.9gtb.com, membership-admin.appstle.com, s.pinimg.com, searchanise-ef84.kxcdn.com, static-tracking.klaviyo.com, static.growave.io, static.klaviyo.com, www.googletagmanager.com
LCP TBT JS KB
as shipped 7.9s 6.7s 3241
apps/3p blocked 3.8s 0.8s 245Nineteen third-party hosts delivered 2.9 MB of JavaScript to a single product page: chat, upsell, cart recovery, loyalty, membership, search, two email-marketing scripts, three public-CDN libraries and an icon font kit. The theme itself was 214 KB. Blocking the apps halved LCP and cut TBT by 88%. That's the ceiling. The realistic plan is fixes 1–5: remove what doesn't earn, delay chat and reviews, consolidate pixels and drop duplicate libraries. Blocking everything can also break features (the page may even look faster because something failed), so verify each change in a real browser.
How BugViso Measures Shopify Speed
A BugViso scan runs the measurements behind each fix on the page you choose (scan a product URL, not just the homepage):
- Core Web Vitals (LCP, CLS, INP, TBT) in a real Chromium load, plus a throttled-network simulation (Slow and Fast 3G profiles with a 4× CPU slowdown) for LCP.
- JavaScript and CSS code coverage: unused bytes per file, flagging files that are more than 40% unused on first render (typical of app bundles).
- Long-task breakdown behind Total Blocking Time, attributed by script container where Chrome provides it.
- Image compression simulation: estimated savings from re-encoding images to WebP or AVIF.
- Network failures split into first- and third-party, which catches broken app scripts.
The report's remediation playbook turns each finding into a fix. Re-scan after each change from your audit records to confirm the gain. You can scan your Shopify product page with BugViso. The metrics are explained on the Core Web Vitals and speed page. For the duplicate-URL side of Shopify, see Shopify duplicate content, and for third-party impact on interactivity, third-party scripts and INP.
Traps and Edge Cases
- A speed score isn't your users' experience. Use real-user data where you have enough traffic, and lab tests for before/after comparisons. Never optimise for a single score.
- Blocking tags can break attribution. Move pixels into Customer events or a tag manager rather than deleting conversion tracking outright.
- "Speed optimizer" apps that add their own script to defer others can help or hurt. Measure with and without.
- Markets and translations load extra scripts per market. Test the product page in your largest market's subfolder.
- Checkout is out of your hands. Shopify hosts and optimizes it. Focus on the theme pages that lead to it.
FAQ
Why is my Shopify store slow?
In our measurements, mostly because of apps and third-party tags: they made up a median 75% of product-page JavaScript, and blocking them cut median lab LCP by 2.7 s. Oversized hero images, web fonts and unused theme features come next.
Do Shopify apps slow down a store?
Many do, especially apps that inject their own scripts from external servers. Apps built on theme app extensions were much lighter in our data (median 53 KB). Uninstall unused apps and remove their leftover code.
What is a good LCP for a Shopify store?
Google's "good" threshold is 2.5 seconds at the 75th percentile of real users. Lab tests on throttled phones read slower than field data, so compare before and after on the same profile.
Does Shopify's theme affect speed?
Yes, but less than apps in our data: the theme's own JavaScript was a median 339 KB, against 2.1 MB from third parties. Choose a lean theme and disable unused features, but start with apps.
Conclusion
Shopify stores are slow mostly because of what's been added to them: audit and remove apps, delay chat and reviews, consolidate pixels, then fix the hero image and fonts, measuring each step on a throttled phone. A BugViso scan of your product page shows where the bytes and blocking time come from.
See where your site stands
Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.