Shopify

Shopify Performance Optimization

Practical techniques for speeding up a Shopify storefront — image handling, render-blocking apps, and theme JS.

Tayyab Roy8 min read
Share:

Shopify stores have a specific performance profile that’s different from a typical custom-built site: the theme code is usually only part of the problem. A client once asked me to “make the site faster” and the single biggest win wasn’t anything in the theme at all — it was three apps injecting render-blocking scripts into theme.liquid that nobody had audited in over a year. Shopify gives you decent infrastructure by default (CDN-served assets, server-side rendering), but it also gives merchants an easy way to bolt on apps that quietly wreck Core Web Vitals. Here’s where the actual performance levers are, roughly in order of impact.

Images: The Biggest Lever

Oversized images are still the most common performance problem on Shopify stores, and it’s almost always fixable with filters you already have available in Liquid. Shopify stores every uploaded image at full resolution and serves resized, CDN-cached versions on demand via image_url (current) or the older img_url:

<!-- Bad: serves the original upload, however large it is -->
<img src="{{ product.featured_image | img_url: 'master' }}" alt="{{ product.title }}">

<!-- Good: request the actual rendered size -->
<img
  src="{{ product.featured_image | image_url: width: 600 }}"
  srcset="
    {{ product.featured_image | image_url: width: 400 }} 400w,
    {{ product.featured_image | image_url: width: 600 }} 600w,
    {{ product.featured_image | image_url: width: 900 }} 900w
  "
  sizes="(min-width: 768px) 600px, 100vw"
  width="600"
  height="600"
  loading="lazy"
  alt="{{ product.featured_image.alt | default: product.title }}"
>

A few things doing real work here: srcset/sizes let the browser pick the right resolution for the actual rendered size and the device’s pixel density, instead of shipping a 2000px-wide image to a 400px card. Explicit width/height attributes prevent layout shift while the image loads, which directly affects Cumulative Layout Shift. loading="lazy" defers requests for offscreen images — but only use it below the fold. Never lazy-load the hero image or the first product image above the fold — that’s your Largest Contentful Paint candidate, and lazy-loading it delays the exact thing LCP is measuring. Use loading="eager" or just omit the attribute for anything in the initial viewport.

For background images set via section settings, the same principle applies — resize through image_url rather than embedding whatever the merchant uploaded:

<div class="hero" style="background-image: url('{{ section.settings.bg_image | image_url: width: 1600 }}')">

Auditing Third-Party Apps

This is the one that surprises people most. Every app a merchant installs that touches the storefront typically injects a <script> tag into the theme, often in theme.liquid’s <head>, and often without defer or async. Reviews widgets, upsell popups, chat widgets, and analytics pixels are the usual offenders. Each one is a render-blocking request before the page can paint.

The audit process:

  1. Open the site in Chrome DevTools, go to the Network tab, and filter by JS. Look at what’s loading before DOMContentLoaded.
  2. Cross-reference against the installed apps list in the Shopify admin — most storefront-injecting apps will have an obviously named script (klaviyo.js, yotpo-reviews.js, etc.).
  3. For each one, ask whether it needs to run before first paint. A reviews widget rendering below the fold almost never does. A live chat bubble doesn’t either.
  4. Where the app gives you control over script placement (some do, via app embed blocks in the theme editor), move it to load after window.onload or with defer.

If you have direct control over the script tag (self-hosted or added manually rather than via an opaque app), defer it explicitly:

<script src="{{ 'reviews-widget.js' | asset_url }}" defer></script>

For apps you don’t control and that don’t offer a defer option, the honest tradeoff is: evaluate whether the app’s value justifies its performance cost, and don’t be afraid to uninstall apps that were added once and forgotten. I’ve found storefronts running five or six apps where the merchant could only name two they actually used.

Deferring Non-Critical Theme JS

The same discipline that applies to apps applies to your own theme code. Not every script needs to load and execute before the page is interactive:

<!-- Critical: needed to make the product form usable immediately -->
<script src="{{ 'product-form.js' | asset_url }}"></script>

<!-- Non-critical: can wait -->
<script src="{{ 'recently-viewed.js' | asset_url }}" defer></script>
<script src="{{ 'social-share.js' | asset_url }}" defer></script>

If you’re following a section-scoped JS architecture — each section loading only its own script, as described in Shopify Theme Development: Architecture and Best Practices — you get a lot of this for free, since a script for a section that isn’t on the current template simply never loads. The failure mode to avoid is a single global theme.js bundling logic for the cart drawer, the product gallery, and the footer newsletter signup into one file that loads in full on every single page.

Minimizing Expensive Liquid Loops

Liquid itself is fast, but naive loops over large collections can add real server-side render time and, in extreme cases, hit Shopify’s resource limits on object access within a single render. A common offender:

<!-- Expensive: iterates every product in a potentially large collection,
     with no limit, just to find a handful matching a tag -->
{% for product in collection.products %}
  {% if product.tags contains 'featured' %}
    {% render 'product-card', product: product %}
  {% endif %}
{% endfor %}

Better: paginate explicitly, or push the filtering to where it belongs — a dedicated collection, a saved search, or a metafield-driven list rather than a tag scan over the whole collection on every render:

{% paginate collection.products by 24 %}
  {% for product in collection.products %}
    {% render 'product-card', product: product %}
  {% endfor %}
{% endpaginate %}

If you genuinely need “featured products from this collection,” the cleaner fix is usually a merchant-curated list (a metaobject or a manually built collection) rather than filtering a few thousand products by tag on every page view. The same applies to nested loops — a loop over collection.products that then loops over each product’s variants to compute something can get expensive fast on collections with hundreds of multi-variant products; precompute what you can, or limit the outer loop with pagination.

Auditing: theme-check and Lighthouse

Two tools should be part of your regular workflow, not a one-time cleanup:

Shopify’s theme-check is a static analysis CLI (and VS Code extension) that catches Liquid-specific issues — deprecated tags like include, missing alt attributes, unused assets, and a number of performance-relevant lint rules. Run it as part of your local dev loop or CI:

shopify theme check

It will flag things like unclosed tags, missing schema validation, and deprecated filters before they ever reach a merchant’s live store — cheaper to catch at lint time than during a performance audit later.

Lighthouse (built into Chrome DevTools, or run via npx lighthouse <url> --view) gives you the Core Web Vitals picture end-to-end: LCP, CLS, and interaction responsiveness, measured against the actual rendered page including every app script. Run it against a real product page and a real collection page, not just the homepage — those are usually where image weight and app scripts pile up. Treat the diagnostics section, not just the top-line score, as the actionable part — it’ll name the specific render-blocking resource or oversized image.

For the general, framework-agnostic version of these techniques — the kind that applies whether or not you’re on Shopify — see Frontend Performance Optimization Techniques. And if part of your stack includes a headless or Next.js-based storefront layer pulling from Shopify’s Storefront API, the image-sizing discipline here has a direct parallel in Next.js Image Optimization — the underlying problem (shipping the right resolution, avoiding layout shift) is the same regardless of which rendering layer is serving the HTML.

Common Mistakes

Resizing images with CSS instead of at the source. width: 100%; height: auto on an <img> whose src is still the full master image means the browser downloads the full file and then shrinks it visually — all the bytes still transfer. Always resize through image_url/img_url first.

Lazy-loading everything indiscriminately. As covered above, lazy-loading your LCP element actively hurts the metric you’re trying to improve.

Assuming Shopify’s CDN makes image optimization unnecessary. The CDN caches and serves whatever size you ask for — it doesn’t choose a sensible size for you. If you request master, you get the master file, cached.

Never re-auditing after launch. Apps get added, removed, and updated by merchants independently of theme development. A storefront that was fast at launch can quietly regress over months as new apps get installed. Build a periodic Lighthouse check into your maintenance routine, not just your launch checklist.

FAQ

Does Shopify Plus or a higher plan make the storefront faster? Plan tier affects checkout customization and API rate limits, not the rendering performance of your theme. Performance is almost entirely a function of theme code and installed apps, not subscription tier.

Is a headless/Hydrogen storefront automatically faster than a Liquid theme? Not automatically — it shifts where the performance risk lives (client-side JS bundle size and hydration cost instead of server-rendered Liquid and app scripts), but a poorly optimized headless build can be slower than a well-optimized Liquid theme. The fundamentals — image sizing, deferring non-critical JS, auditing third-party scripts — apply in both architectures.

#Shopify#Performance#Core Web Vitals

Written by

Tayyab Roy

Senior Frontend / Full-Stack Engineer

Senior Frontend & Full-Stack Engineer specializing in Angular, React, Next.js and enterprise eCommerce (Shopify, SAP Commerce / Hybris, Spartacus). I write about practical, production-tested frontend engineering.