Speed is a revenue lever. A 1-second delay in mobile load time reduces conversions by up to 20% — and on Shopify, where you’re competing with polished DTC brands on every Google search, slow is expensive. Here’s how I approach performance on every build.
Why Performance Matters for E-commerce
Most Shopify store owners think about performance after launch, when the complaints start coming in. I build for it from day one, because retrofitting performance is much harder than designing for it upfront.
The metrics that matter most for Shopify stores are:
- LCP (Largest Contentful Paint) — how fast the hero image or main content loads. Target: under 2.5s.
- CLS (Cumulative Layout Shift) — how much the page jumps around while loading. Target: under 0.1.
- INP (Interaction to Next Paint) — how responsive the page is to taps and clicks. Target: under 200ms.
- TTFB (Time to First Byte) — how fast Shopify’s servers respond. You can’t fully control this, but Shopify’s CDN is generally fast.
Google uses Core Web Vitals as a ranking signal. A fast store isn’t just good for users — it’s good for organic traffic.
Common Shopify Performance Pitfalls
Before I can optimise, I need to audit. These are the issues I find on almost every store I inherit:
Render-blocking scripts — apps injecting <script> tags into <head> without async or defer. Each one stalls the browser parser.
Unoptimised images — product images uploaded at 4000×4000px and displayed at 400×400px. The browser downloads 10x more data than it needs.
App bloat — the average Shopify store has 6–12 apps installed. Each one adds JavaScript, CSS, and often additional network requests. Even inactive apps sometimes leave code behind.
Missing width and height on images — without explicit dimensions, the browser can’t reserve space during load, causing layout shift (CLS).
Loading everything eagerly — all images, all carousels, all tabs loading simultaneously instead of only when needed.
Critical CSS
The fastest CSS is CSS the browser doesn’t have to wait for. My approach:
I identify the styles needed to render the above-the-fold layout — nav, hero, basic typography — and inline them in <head>. Everything else loads asynchronously:
{% comment %} In theme.liquid <head> {% endcomment %}
<style>
/* Critical: nav, hero, typography */
{{ 'critical.css' | asset_url | stylesheet_tag }}
</style>
<link rel="preload" href="{{ 'main.css' | asset_url }}" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="{{ 'main.css' | asset_url }}">
</noscript>
This pattern — preload + onload swap + noscript fallback — is the standard non-blocking CSS load. The browser starts fetching main.css immediately (preload), but doesn’t block rendering on it.
Lazy Loading & Image Optimisation
Shopify’s CDN can resize images on-the-fly. I always use img_url with explicit dimensions and srcset for responsive delivery:
{% assign img_url = product.featured_image | img_url: '1x1' %}
<img
src="{{ product.featured_image | img_url: '400x' }}"
srcset="
{{ product.featured_image | img_url: '400x' }} 400w,
{{ product.featured_image | img_url: '800x' }} 800w,
{{ product.featured_image | img_url: '1200x' }} 1200w
"
sizes="(max-width: 600px) 100vw, 50vw"
width="{{ product.featured_image.width }}"
height="{{ product.featured_image.height }}"
loading="lazy"
alt="{{ product.featured_image.alt | escape }}"
>
The width and height attributes are critical — they tell the browser the image’s aspect ratio before it downloads, eliminating layout shift. The loading="lazy" attribute defers off-screen images. For the hero (LCP image), I use loading="eager" and add a <link rel="preload"> in <head>.
Liquid Rendering Optimisation
Liquid runs on Shopify’s servers, so slow Liquid = slow TTFB. A few patterns I follow:
Avoid N+1 loops — don’t fetch a product inside a loop that iterates products. Instead, use all_products or pre-fetch collections:
{% comment %} Bad — fetches each product separately in the loop {% endcomment %}
{% for product_handle in section.settings.products %}
{% assign product = all_products[product_handle] %}
{% endfor %}
{% comment %} Better — reference a collection directly {% endcomment %}
{% assign feature_collection = collections[section.settings.collection] %}
{% for product in feature_collection.products limit: 4 %}
{%- render 'product-card', product: product -%}
{% endfor %}
Use render instead of include — render isolates the snippet’s variable scope, preventing accidental variable leakage and making the code easier to reason about.
Paginate large collections — never loop over an entire collection without paginate. Shopify caps at 250 products per loop iteration anyway, but even 50 products rendered fully can slow response time:
{% paginate collection.products by 24 %}
{% for product in collection.products %}
{%- render 'product-card', product: product -%}
{% endfor %}
{% endpaginate %}
Measuring Results
I run three checks on every build before launch:
- Lighthouse — in Chrome DevTools, throttled to simulated 4G. I target 85+ on mobile.
- WebPageTest — real browser testing from multiple locations. The filmstrip view shows exactly when content appears.
- Chrome UX Report (CrUX) — field data from real users. Once a store has traffic, this is the ground truth.
Real Numbers
Across my portfolio builds:
| Store | Mobile LCP | Lighthouse Mobile | CLS |
|---|---|---|---|
| Maguire Leathers | 1.6s | 91 | 0.02 |
| Good Art HLYWD | 1.8s | 88 | 0.04 |
| Haus Oslo | 1.4s | 93 | 0.01 |
| The Sneakers Stop | 1.9s | 86 | 0.03 |
These aren’t achieved by cutting features — every store has full carousels, custom sections, and rich product photography. Performance is a discipline, not a compromise.