shopify-performance
Shopify Performance Optimization
Expert guidance for optimizing Shopify store performance including theme speed, asset optimization, and Core Web Vitals.
When to Use This Skill
Invoke this skill when:
- Optimizing Shopify theme performance and load times
- Improving Lighthouse or PageSpeed Insights scores
- Reducing Time to Interactive (TTI) or Largest Contentful Paint (LCP)
- Optimizing images for faster loading
- Implementing lazy loading for images and videos
- Minifying and optimizing JavaScript and CSS
- Reducing JavaScript bundle sizes
- Improving Core Web Vitals metrics (LCP, FID, CLS)
- Implementing caching strategies
- Optimizing Liquid template rendering
- Reducing server response times
- Improving mobile performance
Core Capabilities
1. Image Optimization
Images are typically the largest assets - optimize aggressively.
Use Shopify CDN Image Sizing:
{# ❌ Don't load full-size images #}
<img src="{{ product.featured_image.src }}" alt="{{ product.title }}">
{# ✅ Use img_url filter with appropriate size #}
<img
src="{{ product.featured_image | img_url: '800x800' }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
width="800"
height="800"
>
Responsive Images:
<img
src="{{ image | img_url: '800x' }}"
srcset="
{{ image | img_url: '400x' }} 400w,
{{ image | img_url: '800x' }} 800w,
{{ image | img_url: '1200x' }} 1200w,
{{ image | img_url: '1600x' }} 1600w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
alt="{{ image.alt | escape }}"
loading="lazy"
width="800"
height="800"
>
Modern Image Formats:
<picture>
{# WebP for modern browsers #}
<source
type="image/webp"
srcset="
{{ image | img_url: '400x', format: 'pjpg' }} 400w,
{{ image | img_url: '800x', format: 'pjpg' }} 800w
"
>
{# Fallback to JPEG #}
<img
src="{{ image | img_url: '800x' }}"
srcset="
{{ image | img_url: '400x' }} 400w,
{{ image | img_url: '800x' }} 800w
"
alt="{{ image.alt | escape }}"
loading="lazy"
>
</picture>
Lazy Loading:
{# Native lazy loading #}
<img
src="{{ image | img_url: '800x' }}"
alt="{{ image.alt | escape }}"
loading="lazy"
decoding="async"
>
{# Eager load above-the-fold images #}
{% if forloop.index <= 3 %}
<img src="{{ image | img_url: '800x' }}" loading="eager">
{% else %}
<img src="{{ image | img_url: '800x' }}" loading="lazy">
{% endif %}
Preload Critical Images:
{# In <head> for hero images #}
<link
rel="preload"
as="image"
href="{{ section.settings.hero_image | img_url: '1920x' }}"
imagesrcset="
{{ section.settings.hero_image | img_url: '800x' }} 800w,
{{ section.settings.hero_image | img_url: '1920x' }} 1920w
"
imagesizes="100vw"
>
2. JavaScript Optimization
Reduce JS payload and execution time.
Defer Non-Critical JavaScript:
{# ❌ Blocking JavaScript #}
<script src="{{ 'theme.js' | asset_url }}"></script>
{# ✅ Deferred JavaScript #}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
{# ✅ Async for independent scripts #}
<script src="{{ 'analytics.js' | asset_url }}" async></script>
Inline Critical JavaScript:
{# Inline small, critical scripts #}
<script>
// Critical initialization code
document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js');
</script>
Code Splitting:
// Load features only when needed
async function loadCart() {
const { Cart } = await import('./cart.js');
return new Cart();
}
// Load on interaction
document.querySelector('.cart-icon').addEventListener('click', async () => {
const cart = await loadCart();
cart.open();
}, { once: true });
Remove Unused JavaScript:
// ❌ Don't load libraries you don't use
// Example: Don't include entire jQuery if you only need a few functions
// ✅ Use native alternatives
// Instead of: $('.selector').hide()
// Use: document.querySelector('.selector').style.display = 'none';
// Instead of: $.ajax()
// Use: fetch()
Minify JavaScript:
# Use build tools to minify
npm install terser --save-dev
# Minify
terser theme.js -o theme.min.js -c -m
3. CSS Optimization
Optimize stylesheets for faster rendering.
Critical CSS:
{# Inline critical above-the-fold CSS in <head> #}
<style>
/* Critical CSS only (header, hero) */
.header { /* ... */ }
.hero { /* ... */ }
.button { /* ... */ }
</style>
{# Load full CSS deferred #}
<link
rel="preload"
href="{{ 'theme.css' | asset_url }}"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
>
<noscript>
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
</noscript>
Remove Unused CSS:
# Use PurgeCSS to remove unused styles
npm install @fullhuman/postcss-purgecss --save-dev
# Configure in postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./**/*.liquid'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
}),
],
};
Minify CSS:
# Use cssnano
npm install cssnano --save-dev
# Minify
npx cssnano style.css style.min.css
Avoid @import:
/* ❌ Don't use @import (blocks rendering) */
@import url('fonts.css');
/* ✅ Use multiple <link> tags instead */
<link rel="stylesheet" href="{{ 'main.css' | asset_url }}">
<link rel="stylesheet" href="{{ 'fonts.css' | asset_url }}">
4. Font Optimization
Optimize web fonts for faster text rendering.
Font Loading:
{# Preload fonts #}
<link
rel="preload"
href="{{ 'font.woff2' | asset_url }}"
as="font"
type="font/woff2"
crossorigin
>
{# Font face with font-display #}
<style>
@font-face {
font-family: 'CustomFont';
src: url('{{ 'font.woff2' | asset_url }}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* Show fallback font immediately */
}
</style>
System Font Stack:
/* Use system fonts for instant rendering */
body {
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif;
}
Subset Fonts:
/* Load only required characters */
@font-face {
font-family: 'CustomFont';
src: url('font-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
5. Liquid Template Optimization
Optimize Liquid rendering for faster server response.
Cache Expensive Operations:
{# ❌ Repeated calculations #}
{% for i in (1..10) %}
{{ collection.products.size }} {# Calculated 10 times #}
{% endfor %}
{# ✅ Cache result #}
{% assign product_count = collection.products.size %}
{% for i in (1..10) %}
{{ product_count }}
{% endfor %}
Use limit and offset:
{# ❌ Iterate full array and break #}
{% for product in collection.products %}
{% if forloop.index > 5 %}{% break %}{% endif %}
{{ product.title }}
{% endfor %}
{# ✅ Use limit #}
{% for product in collection.products limit: 5 %}
{{ product.title }}
{% endfor %}
Avoid Nested Loops:
{# ❌ O(n²) complexity #}
{% for product in collection.products %}
{% for variant in product.variants %}
{# Expensive nested loop #}
{% endfor %}
{% endfor %}
{# ✅ Flatten or preprocess #}
{% assign all_variants = collection.products | map: 'variants' | flatten %}
{% for variant in all_variants limit: 50 %}
{{ variant.title }}
{% endfor %}
Prefer render over include:
{# ❌ include (slower, shared scope) #}
{% include 'product-card' %}
{# ✅ render (faster, isolated scope) #}
{% render 'product-card', product: product %}
Use section-specific stylesheets:
{# Scope CSS to section for better caching #}
{% stylesheet %}
.my-section { /* ... */ }
{% endstylesheet %}
{# Scope JavaScript to section #}
{% javascript %}
class MySection { /* ... */ }
{% endjavascript %}
6. Third-Party Script Optimization
Minimize impact of external scripts.
Defer Third-Party Scripts:
{# ❌ Blocking third-party script #}
<script src="https://external.com/script.js"></script>
{# ✅ Async or defer #}
<script src="https://external.com/script.js" async></script>
{# ✅ Load on user interaction #}
<script>
let gaLoaded = false;
function loadGA() {
if (gaLoaded) return;
const script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=GA_ID';
script.async = true;
document.head.appendChild(script);
gaLoaded = true;
}
// Load on scroll or after delay
window.addEventListener('scroll', loadGA, { once: true });
setTimeout(loadGA, 3000);
</script>
Use Facade Pattern:
{# Show placeholder instead of embedding heavy iframe #}
<div class="video-facade" data-video-id="abc123">
<img src="thumbnail.jpg" alt="Video">
<button onclick="loadVideo(this)">Play Video</button>
</div>
<script>
function loadVideo(btn) {
const facade = btn.parentElement;
const videoId = facade.dataset.videoId;
const iframe = document.createElement('iframe');
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
facade.replaceWith(iframe);
}
</script>
7. Caching Strategies
Leverage browser and CDN caching.
Asset Versioning:
{# Shopify auto-versions assets #}
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
{# Outputs: /cdn/.../theme.css?v=12345678 #}
Long Cache Headers:
{# Shopify CDN sets appropriate cache headers #}
{# CSS/JS: 1 year #}
{# Images: 1 year #}
Service Worker (Advanced):
// sw.js - Cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/cdn/.../theme.css',
'/cdn/.../theme.js',
'/cdn/.../logo.png',
]);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
8. Core Web Vitals Optimization
Improve Google's Core Web Vitals metrics.
Largest Contentful Paint (LCP):
{# Optimize largest element load time #}
{# 1. Preload hero image #}
<link rel="preload" as="image" href="{{ hero_image | img_url: '1920x' }}">
{# 2. Use priority hint #}
<img src="{{ hero_image | img_url: '1920x' }}" fetchpriority="high">
{# 3. Optimize server response time (use Shopify CDN) #}
{# 4. Remove render-blocking resources #}
<script src="theme.js" defer></script>
First Input Delay (FID) / Interaction to Next Paint (INP):
// 1. Reduce JavaScript execution time
// 2. Break up long tasks
function processItems(items) {
// ❌ Long task
items.forEach(item => processItem(item));
// ✅ Break into smaller chunks
async function processInChunks() {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield to main thread every 50 items
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
processInChunks();
}
// 3. Use requestIdleCallback
requestIdleCallback(() => {
// Non-critical work
});
Cumulative Layout Shift (CLS):
{# 1. Always set width and height on images #}
<img
src="{{ image | img_url: '800x' }}"
width="800"
height="600"
alt="Product"
>
{# 2. Reserve space for dynamic content #}
<div style="min-height: 400px;">
{# Content loads here #}
</div>
{# 3. Use aspect-ratio for responsive images #}
<style>
.image-container {
aspect-ratio: 16 / 9;
}
</style>
9. Performance Monitoring
Track performance metrics.
Measure Core Web Vitals:
// Load web-vitals library
import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
// Send to analytics
gtag('event', name, {
event_category: 'Web Vitals',
event_label: id,
value: Math.round(name === 'CLS' ? value * 1000 : value),
});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
Performance Observer:
// Monitor long tasks
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', entry.duration, 'ms');
}
});
observer.observe({ entryTypes: ['longtask'] });
Performance Checklist
Images:
- Use
img_urlfilter with appropriate sizes - Implement responsive images with
srcset - Add
loading="lazy"to below-fold images - Set explicit
widthandheightattributes - Preload critical hero images
- Use modern formats (WebP)
JavaScript:
- Defer or async all non-critical scripts
- Minify and bundle JavaScript
- Code-split large bundles
- Remove unused code
- Lazy load features on interaction
CSS:
- Inline critical CSS
- Defer non-critical CSS
- Remove unused styles
- Minify stylesheets
- Avoid
@import
Fonts:
- Preload critical fonts
- Use
font-display: swap - Consider system font stack
- Subset fonts when possible
Third-Party:
- Audit all third-party scripts
- Load scripts async or on interaction
- Use facade pattern for heavy embeds
- Monitor third-party impact
Liquid:
- Cache expensive calculations
- Use
limitinstead of manual breaks - Prefer
renderoverinclude - Avoid nested loops
Core Web Vitals:
- LCP < 2.5s
- FID < 100ms (INP < 200ms)
- CLS < 0.1
Best Practices
- Test on real devices - Mobile 3G performance matters
- Use Lighthouse for performance audits
- Monitor Core Web Vitals in production
- Optimize above-the-fold content first
- Lazy load everything else below the fold
- Minimize main thread work for better interactivity
- Use Shopify CDN for all assets
- Version assets for effective caching
- Compress images before uploading
- Regular performance audits to catch regressions
Integration with Other Skills
- shopify-liquid - Use when optimizing Liquid template code
- shopify-theme-dev - Use when organizing theme assets
- shopify-debugging - Use when troubleshooting performance issues
- shopify-api - Use when optimizing API request patterns
Quick Reference
{# Images #}
<img src="{{ image | img_url: '800x' }}" loading="lazy" width="800" height="800">
{# Scripts #}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
{# Fonts #}
<link rel="preload" href="{{ 'font.woff2' | asset_url }}" as="font" crossorigin>
{# Critical CSS #}
<style>/* Critical CSS */</style>
<link rel="preload" href="{{ 'theme.css' | asset_url }}" as="style" onload="this.rel='stylesheet'">
{# Responsive images #}
<img srcset="{{ image | img_url: '400x' }} 400w, {{ image | img_url: '800x' }} 800w">