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.

BugViso

14 min read

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

ItemDetail
Stores64 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)
PageOne product page per store (from the store's first collection)
Profile412×823 mobile viewport, mobile Chrome user agent, 150 ms RTT, 1.6 Mbps down / 750 kbps up, 4× CPU slowdown; one load per variant
VariantsAs shipped, and apps blocked: requests to cdn.shopify.com/extensions/… (theme app embeds) and to every non-Shopify third-party host aborted
JavaScript bytesEncoded transfer size of each script request from Chrome DevTools Protocol network events, grouped by origin
Product page, median of 23 storesAs shippedApps & third-party blocked
Largest Contentful Paint (lab)7.5 s4.7 s
Total Blocking Time (lab)5.4 s1.4 s
Stores with lab LCP ≤ 2.5 s23
Per-store LCP saving (median)2.7 s
Per-store TBT saving (median)4.6 s
Stores where blocking saved > 1 s of LCP20 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
Total2,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:

html
<!-- ✅ 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

liquid
{% 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:

python
#!/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):

text
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      245

Nineteen 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.

Found this useful? Share it.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.