DEV Community

Evgenii Milevich
Evgenii Milevich

Posted on

Shopify Performance Optimization: A Developer's Checklist for 2026

Last quarter we took a client's Shopify store from a PageSpeed Insights score of 35 to 92. Mobile. On a theme with 14 apps installed. This article is the exact process we followed, written as a checklist you can use on your own stores.

A few things before we start. Shopify is a hosted platform — you don't control the server, the CDN config, or the TLS handshake. That means roughly 30% of your total page load time is outside your control. The good news: the remaining 70% has so much low-hanging fruit that most stores can double their performance score without touching anything architectural.

We do this work regularly at MILEDEVS as part of our Shopify development services. Every recommendation below comes from production stores, not lab conditions.


1. Image Optimization — The Biggest Win

Images account for 50-80% of total page weight on a typical Shopify store. Fix this first.

Use Shopify's Built-in Image CDN Properly

Shopify automatically serves images through their CDN, but only if you use the image_url filter instead of hardcoded URLs:

{% comment %} Bad: hardcoded, no CDN optimization {% endcomment %}
<img src="{{ product.featured_image | img_url: 'master' }}" />

{% comment %} Good: responsive, WebP-auto, CDN-optimized {% endcomment %}
<img
  src="{{ product.featured_image | image_url: width: 600 }}"
  srcset="
    {{ product.featured_image | image_url: width: 300 }} 300w,
    {{ product.featured_image | image_url: width: 600 }} 600w,
    {{ product.featured_image | image_url: width: 900 }} 900w,
    {{ product.featured_image | image_url: width: 1200 }} 1200w
  "
  sizes="(max-width: 768px) 100vw, 50vw"
  loading="lazy"
  decoding="async"
  width="{{ product.featured_image.width }}"
  height="{{ product.featured_image.height }}"
  alt="{{ product.featured_image.alt | escape }}"
/>
Enter fullscreen mode Exit fullscreen mode

Key details:

  • image_url (not img_url) automatically converts to WebP when the browser supports it
  • The width and height attributes prevent layout shift — the browser reserves space before the image loads
  • loading="lazy" defers off-screen images. Do NOT lazy-load the first visible image (hero, first product in grid) — that will hurt LCP
  • decoding="async" lets the browser decode images off the main thread

Upload Source Images at 2x Maximum Display Size

If your product images display at 600px, upload at 1200px. Not 4000px. Shopify resizes on the fly, but it still has to process larger source files, and you'll sometimes see the original loaded by poorly written app code or social sharing previews.

Our result: Replacing img_url: 'master' with properly sized image_url across a fashion store reduced page weight from 8.2 MB to 1.4 MB. LCP dropped from 6.1s to 2.3s.


2. JavaScript Audit — Find and Kill What You Don't Need

Identify the Bloat

Open Chrome DevTools, go to the Coverage tab (Ctrl+Shift+P, type "Coverage"), reload the page. The red bars show unused bytes. On a typical Shopify store, 60-70% of loaded JavaScript is unused on any given page.

The usual culprits:

Source Typical Size Often Unnecessary On
App scripts 200-800 KB Pages where the app isn't used
Theme vendor.js 100-300 KB Contains unused jQuery plugins
Analytics (GA4, Meta, TikTok) 150-400 KB N/A (but can be deferred)
Chat widgets 200-500 KB Every page except where needed

Defer Non-Critical Scripts

Shopify's content_for_header outputs all app scripts synchronously. You can't change that directly, but you can defer your own theme scripts:

{% comment %} layout/theme.liquid {% endcomment %}

{% comment %} Critical: render-blocking, needed for first paint {% endcomment %}
<script src="{{ 'critical.js' | asset_url }}" defer></script>

{% comment %} Non-critical: load after page is interactive {% endcomment %}
<script>
  window.addEventListener('load', function() {
    var scripts = [
      '{{ "deferred-features.js" | asset_url }}',
      '{{ "reviews-widget.js" | asset_url }}'
    ];
    scripts.forEach(function(src) {
      var s = document.createElement('script');
      s.src = src;
      s.async = true;
      document.body.appendChild(s);
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Remove jQuery If You Can

Dawn and most modern Shopify themes don't require jQuery. If your theme still loads it, check whether any critical functionality depends on it. Many stores load jQuery (90 KB minified) solely because one old app snippet uses $('.something'). Replace those few lines with vanilla JS and drop the entire library.

// Before (requires jQuery)
$('.accordion-toggle').click(function() {
  $(this).next('.accordion-content').slideToggle();
});

// After (vanilla JS, 0 dependencies)
document.querySelectorAll('.accordion-toggle').forEach(function(toggle) {
  toggle.addEventListener('click', function() {
    const content = this.nextElementSibling;
    content.style.display = content.style.display === 'none' ? 'block' : 'none';
  });
});
Enter fullscreen mode Exit fullscreen mode

3. Liquid Profiling — Find Slow Templates

Enable Shopify's Liquid Profiler

Add ?_fd=0&pb=0 to any page URL on your development store to see raw Liquid render times in the response. For more detailed profiling:

https://your-store.myshopify.com/?profile_liquid=true
Enter fullscreen mode Exit fullscreen mode

This adds HTML comments showing render time for every snippet and section. Look for anything over 5ms — that's slow for a Liquid partial.

Common Liquid Performance Traps

Trap 1: Nested loops over large collections

{% comment %} SLOW: O(n*m) — checks every product against every tag {% endcomment %}
{% for product in collections.all.products %}
  {% for tag in product.tags %}
    {% if tag == 'featured' %}
      {% render 'product-card', product: product %}
    {% endif %}
  {% endfor %}
{% endfor %}

{% comment %} FAST: Use a filtered collection instead {% endcomment %}
{% for product in collections.featured.products limit: 12 %}
  {% render 'product-card', product: product %}
{% endfor %}
Enter fullscreen mode Exit fullscreen mode

Trap 2: Rendering the same snippet hundreds of times in a loop

Every {% render %} call has overhead. If you're rendering a product card 50 times in a collection page, make sure the snippet itself is lean — no nested render calls, no redundant assign statements.

Trap 3: Using where filter on large arrays

{% comment %} SLOW on arrays with 1000+ items {% endcomment %}
{% assign filtered = product.metafields.custom.related | where: "status", "active" %}

{% comment %} FASTER: filter server-side via collection or metafield definition {% endcomment %}
Enter fullscreen mode Exit fullscreen mode

4. App Audit — The Hidden Performance Tax

Every Shopify app injects code into your storefront. Most store owners have no idea how much.

The Audit Process

  1. List all installed apps — go to Settings > Apps and sales channels
  2. Check which apps inject frontend code — in theme editor, look at content_for_header output (View Source, search for <script)
  3. Test removal impact — disable apps one at a time and measure PageSpeed after each

What We Typically Find

On the store we took from 35 to 92, here's what the app audit revealed:

  • Review app: Loading 340 KB of JS on every page, including pages with no reviews. Fix: load conditionally only on product pages using Shopify's app embed controls.
  • Currency converter: 180 KB. The theme had built-in currency support. App was redundant. Removed.
  • Exit-intent popup: 220 KB. Replaced with a 3 KB custom implementation using mouseleave event.
  • Two abandoned analytics apps the client had forgotten about: 280 KB combined. Removed.

Total savings: 1,020 KB of JavaScript. That alone moved the score from 35 to 58.

Replace Heavy Apps With Lightweight Code

Many common app features can be replicated with 20-50 lines of Liquid and JavaScript:

  • Announcement bars — a <div> with inline styles and a dismiss cookie
  • Back-in-stock buttons — a form that posts to a Shopify Flow webhook
  • Size charts — a modal with a static HTML table
  • Trust badges — SVG icons in a snippet

Don't replace complex apps (reviews, subscriptions, loyalty programs) — those have backend logic that justifies their weight. But purely frontend features often don't need 200 KB of vendor JavaScript.


5. Core Web Vitals — The Metrics That Matter

Google uses three Core Web Vitals for ranking: LCP (Largest Contentful Paint), INP (Interaction to Next Paint), and CLS (Cumulative Layout Shift).

LCP: Target Under 2.5 Seconds

The largest contentful paint is almost always your hero image or the first product image. To optimize:

{% comment %} Preload the hero image — tells the browser to fetch it immediately {% endcomment %}
{% if template.name == 'index' %}
  <link
    rel="preload"
    as="image"
    href="{{ section.settings.hero_image | image_url: width: 1200 }}"
    type="image/webp"
    fetchpriority="high"
  />
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Also add fetchpriority="high" to the hero <img> tag itself:

<img src="..." fetchpriority="high" loading="eager" />
Enter fullscreen mode Exit fullscreen mode

Do NOT lazy-load your LCP element. This is the single most common mistake we see. The loading="lazy" attribute tells the browser to deprioritize the image — the exact opposite of what you want for LCP.

INP: Target Under 200ms

Interaction to Next Paint measures how fast the page responds to user input. Common fixes:

  • Break up long tasks. If a click handler does heavy DOM manipulation, use requestAnimationFrame or scheduler.yield() to avoid blocking the main thread for more than 50ms.
  • Debounce search inputs. Don't fire a search query on every keystroke.
  • Use content-visibility: auto on below-fold sections to reduce rendering work.
/* Skip rendering for off-screen sections */
.section--below-fold {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;
}
Enter fullscreen mode Exit fullscreen mode

CLS: Target Under 0.1

Layout shift happens when elements move after the page has started rendering. Fixes:

  • Always set width/height on images (covered in Section 1)
  • Reserve space for app embeds — if a reviews widget loads asynchronously, wrap its container in a div with a min-height
  • Font loading — use font-display: swap and preload your primary font:
<link
  rel="preload"
  href="{{ 'custom-font.woff2' | asset_url }}"
  as="font"
  type="font/woff2"
  crossorigin
/>
Enter fullscreen mode Exit fullscreen mode
@font-face {
  font-family: 'CustomFont';
  src: url('custom-font.woff2') format('woff2');
  font-display: swap;
}
Enter fullscreen mode Exit fullscreen mode

The Full Checklist

Here's the condensed version you can copy into your project management tool:

  • [ ] Replace all img_url with image_url + srcset + sizes
  • [ ] Add width/height attributes to every <img> tag
  • [ ] Set loading="lazy" on all images EXCEPT the LCP element
  • [ ] Preload hero/LCP image with fetchpriority="high"
  • [ ] Run JS coverage analysis — identify unused bytes
  • [ ] Defer all non-critical theme JavaScript
  • [ ] Remove jQuery if no critical features depend on it
  • [ ] Audit all installed apps — remove unused ones
  • [ ] Check which apps load JS globally vs. per-page
  • [ ] Replace simple app features with lightweight code
  • [ ] Profile Liquid render times — fix templates over 5ms
  • [ ] Eliminate nested loops over large collections
  • [ ] Add content-visibility: auto to below-fold sections
  • [ ] Preload primary web font
  • [ ] Test with PageSpeed Insights on mobile (not just desktop)
  • [ ] Verify Core Web Vitals in Google Search Console (field data, not lab)

Measuring Results

Lab scores (Lighthouse, PageSpeed Insights) are useful for development, but field data from Chrome UX Report (CrUX) is what Google uses for ranking. After deploying performance changes, wait 28 days for CrUX data to update before declaring victory.

Check your field data in Google Search Console under Core Web Vitals, or use the CrUX API directly:

curl "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"origin": "https://your-store.com"}'
Enter fullscreen mode Exit fullscreen mode

The store we optimized from 35 to 92 saw its CrUX "good URLs" percentage go from 12% to 89% over the following month. Organic traffic increased 23% in the same period — not entirely attributable to performance, but the correlation was strong.


Final Thoughts

Performance optimization on Shopify is not a one-time project. Every new app install, every theme update, every seasonal campaign can regress your scores. Build performance checks into your development workflow — run Lighthouse in CI, monitor CrUX monthly, and audit apps quarterly.

If you want a team to handle this end-to-end, we do exactly this at MILEDEVS. But this checklist gives you everything you need to do it yourself. Start with images and the app audit — those two alone will get you 80% of the gains.

Top comments (0)