In the Shopify Architecture Guide, we cover the basics. Today we're dealing with a fact many ignore: Performance is architecture.
At smplx., we worked with J.Clay and achieved a +107% revenue increase through performance optimizations. That wasn't luck -- it was systematic architecture work.
We want to share this lesson with you.
Why Performance Is Architecture
The thesis: Fast stores are not the result of fast servers. They are the result of thoughtful architecture decisions.
The numbers:
- Every 100ms delay = 0.7% fewer conversions (Google)
- 53% leave stores if they take over 3 seconds
- Mobile users are 2x more sensitive to slow performance
But here's the more important truth: These numbers don't come from faster servers. They come from:
- What you load
- When you load it
- Whether the browser can cache it
That's architecture, not DevOps.
Core Web Vitals: The Metrics That Matter
Google measures three metrics:
1. LCP (Largest Contentful Paint)
What is it? When is the largest visible content loaded and visible?
Benchmark:
- Good: < 2.5s
- Acceptable: 2.5-4.0s
- Poor: > 4.0s
What affects LCP?
- Images (the large hero graphic)
- Text (if not embedded in HTML)
- Videos
- Background images with CSS
2. FID (First Input Delay) / INP (Interaction to Next Paint)
What is it? How long does it take for the browser to respond to your interaction?
Benchmark:
- Good: < 100ms
- Acceptable: 100-300ms
- Poor: > 300ms
What affects INP?
- JavaScript execution (too much JS blocks the main thread)
- Long Tasks (more than 50ms JavaScript without a pause)
- Missing event listeners
3. CLS (Cumulative Layout Shift)
What is it? Does your page jump around during loading?
Benchmark:
- Good: < 0.1
- Acceptable: 0.1-0.25
- Poor: > 0.25
What affects CLS?
- Images without width/height
- Fonts that change
- Ads that appear
- Cookie banners
Critical Rendering Path: The Foundation
What is it? The order in which the browser renders your page.
1. Parse HTML
2. Parse CSS (blocks rendering!)
3. Execute JavaScript (blocks rendering!)
4. Render first pixels (LCP start)
5. Become interactive (INP relevant)
6. Everything loaded (LCP end)
The architecture decision: Minimize blocking.
Liquid and Critical CSS
<!-- WRONG: Everything is blocked -->
<link rel="stylesheet" href="/cdn/all.css">
<script src="/cdn/all.js"></script>
<!-- RIGHT: Critical inline, rest async -->
<style>
/* Only Critical CSS: Header, Hero, Above-Fold */
.header { ... }
.hero { ... }
.button { ... }
</style>
<link rel="stylesheet" href="/cdn/non-critical.css" media="print" onload="this.media='all'">
<script defer src="/cdn/main.js"></script>
<script async src="/cdn/analytics.js"></script>
What this does:
- Critical CSS doesn't block
- Non-critical CSS loads in the background
- JavaScript loads with
defer(after HTML parse) - Analytics with
async(not critical at all)
Image Strategy: The Biggest Performance Lever
The reality: Images are 60-80% of the bandwidth on Shopify stores.
Responsive Images with Picture
<!-- WRONG: One size for all -->
<img src="{{ product.featured_image.src }}" alt="{{ product.featured_image.alt }}">
<!-- RIGHT: WebP, multiple sizes -->
<picture>
<source
type="image/webp"
srcset="{{ product.featured_image | img_url: '300x' }} 300w,
{{ product.featured_image | img_url: '600x' }} 600w,
{{ product.featured_image | img_url: '900x' }} 900w"
sizes="(max-width: 768px) 100vw, 50vw">
<img
src="{{ product.featured_image | img_url: '600x' }}"
alt="{{ product.featured_image.alt }}"
width="600"
height="400"
loading="lazy"
decoding="async">
</picture>
What this achieves:
- WebP for browsers that support it
- Responsive sizes based on device
loading="lazy"= not in viewport loads laterdecoding="async"= doesn't block the main thread
Shopify's Image Delivery API
<!-- With Shopify's native transformation -->
{{ product.featured_image | image_url: width: 600 }}
Shopify delivers images from the CDN with automatic transformations. This is better than external image optimizers.
Font Strategy: Often Overlooked
Fonts are usually >50KB. They can block LCP!
<!-- WRONG: Google Fonts blocks -->
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet">
<!-- RIGHT: Preload + Swap -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
Or even better: Self-host your fonts.
<!-- EVEN BETTER: Self-Hosted -->
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}
Why font-display: swap? The browser shows the fallback font immediately, then swaps it out. No blank LCP phase.
Third-Party Scripts: The Hidden Cost Center
The reality: An average Shopify store loads 30+ external scripts.
That is not OK. Every external script:
- Needs a DNS lookup
- Needs an HTTP request
- Loads extra JavaScript
- Competes with your JavaScript
Perform a Script Audit
// In the browser console
performance.getEntriesByType('resource')
.filter(r => r.initiatorType === 'script')
.forEach(r => {
console.log(r.name, Math.round(r.duration) + 'ms', r.transferSize);
});
What comes out?
- Google Analytics: ~200ms
- Facebook Pixel: ~300ms
- Attentive (Popups): ~400ms
- Custom Font Loader: ~150ms
- ...
- Total: 3+ seconds just for third-party!
Script Architecture
<!-- WRONG: Everything immediately -->
<script src="analytics.js"></script>
<script src="pixel.js"></script>
<script src="popup.js"></script>
<script src="chat.js"></script>
<!-- RIGHT: Lazy Loading -->
<script>
// Analytics immediately (it's important)
// But with async
const script = document.createElement('script');
script.src = '/analytics.js';
script.async = true;
document.head.appendChild(script);
// Popup/Chat: Only when interactive
window.addEventListener('DOMContentLoaded', () => {
loadScript('/popup.js');
});
// Third-party after 5 seconds
setTimeout(() => {
loadScript('/pixel.js');
}, 5000);
</script>
That's architecture: What's critical? What can wait?
Caching: The Underestimated Lever
HTTP Caching for Theme Assets
<!-- In theme liquid (e.g., theme.liquid) -->
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}" media="all">
Shopify serves assets with long cache headers. That's good. But:
- Images: Change often > short cache
- CSS: Changes rarely > long cache
- JavaScript: Changes regularly > medium cache
Browser Caching with Liquid
{% if request.host == 'shop.example.com' %}
<!-- Already cached, can be aggressive -->
<img src="{{ product.image | img_url: '300x' }}" loading="lazy">
{% endif %}
Service Worker for Offline
// If you have a headless store (Next.js/Remix)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// In the Service Worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/products',
'/collections'
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});
This allows offline navigation and faster loads through caching.
Code Splitting: Only Load What's Needed
Liquid with Conditional JavaScript
{% if page.handle == 'contact' %}
<script src="{{ 'form-validation.js' | asset_url }}"></script>
{% endif %}
{% if template == 'product' %}
<script src="{{ 'product-gallery.js' | asset_url }}"></script>
{% endif %}
Headless (Next.js) with Dynamic Imports
// components/ProductGallery.jsx
import dynamic from 'next/dynamic';
const GalleryComponent = dynamic(
() => import('./gallery-component'),
{ loading: () => <p>Loading...</p> }
);
export default function ProductPage() {
return (
<>
<ProductInfo />
<GalleryComponent />
</>
);
}
The browser only loads gallery-component on the product page.
Lazy Loading Sections: Only Render Above-Fold
In Liquid/Traditional Theme
<!-- Above-Fold: Render -->
<section class="hero">
...
</section>
<!-- Below-Fold: Lazy-Load -->
<div
class="lazy-section"
data-src="{% section 'product-carousel' %}">
Loads later...
</div>
<script>
// Intersection Observer: Only load when visible
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const section = entry.target;
const html = document.querySelector(`[data-src="${section.dataset.src}"]`).dataset.src;
section.innerHTML = html;
observer.unobserve(section);
}
});
});
document.querySelectorAll('.lazy-section').forEach(section => {
observer.observe(section);
});
</script>
Effect: A 10-section page is initially loaded with only 2-3 sections.
Case Study: J.Clay [+107% Revenue]
J.Clay needed speed. Here's what we did:
Before
- LCP: 3.8s
- FID: 280ms
- CLS: 0.15
- Conversion: 2.1%
Architecture Changes
Image Strategy (biggest impact)
- WebP + Responsive Sizes
- Lazy Loading for below-fold
- Result: -1.2s LCP
Third-Party Cleanup
- Pixel removed (not ROI-positive)
- Analytics with
async - Popup script lazily loaded
- Result: -0.6s LCP, -150ms FID
Critical CSS Inline
- Header + Hero + CTA inline
- Rest with media="print" trick
- Result: -0.4s LCP
Code Splitting
- Purchase flow script only on checkout
- Product gallery only on product pages
- Result: -100ms INP
After
- LCP: 1.1s
- FID: 85ms
- CLS: 0.08
- Conversion: 4.3% (+107%!)
That wasn't "faster servers." That was architecture.
Performance Monitoring: Don't Build and Forget
Liquid-Based Tracking
<script>
window.addEventListener('load', () => {
const metrics = {
lcp: performance.getEntriesByName('largest-contentful-paint')[0]?.renderTime,
fid: performance.getEntriesByType('first-input')[0]?.processingStart,
cls: Array.from(document.querySelectorAll('[style*="transform"]')).length
};
// Send to analytics
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(metrics)
});
});
</script>
With Web Vitals Library (Recommended)
import { getLCP, getFID, getCLS } from 'web-vitals';
getLCP(console.log);
getFID(console.log);
getCLS(console.log);
Checklist: Performance Architecture Review
Use this for your next project:
- LCP under 2.5s (Lighthouse)
- All images responsive + WebP
- No images without width/height
- Critical CSS inline, rest deferred
- Fonts with
font-display: swap - Third-party scripts only when needed, async/defer
- Code splitting by template/page
- Service Worker for offline (if headless)
- Lazy loading for below-fold sections
- Caching headers configured
- Web Vitals monitoring active
- Lighthouse Score > 85
About the Author
Claudio Gerlich is Technical Architect at smplx. and has specialized in Shopify since 2020. With J.Clay, we've proven that +100% revenue increase through thoughtful performance architecture is possible.
Performance isn't luck. It's decisions. Good decisions.
smplx. is a Shopify Technical Partner (since 2020) based in Munsterland, NRW.