This case study walks through a complete Shopify speed optimization project on a mid-size fashion store - from a mobile PageSpeed score of 31 to 79 over three weeks. The store had 18 installed apps, uncompressed product images averaging 1.8MB each, a slider loading 5 hero images on every page visit, and third-party tracking scripts from 9 separate domains. Every fix is documented with before and after metrics, the tools used, and the specific decisions made at each stage.
The Store: Starting Point and Context
The store is a direct-to-consumer women's clothing brand that had been running for three years on Shopify Plus. The catalog contained approximately 340 active products, most with 3 to 6 color variants and 5 to 8 images per product. Monthly traffic was around 85,000 sessions, with mobile traffic at 71 percent of total sessions.
The store was running a premium third-party theme purchased two years earlier, which the team had customized heavily for seasonal promotions.
Starting baseline metrics (average of 3 PageSpeed tests):
| Page | Mobile Score | LCP | CLS | INP | Page Weight |
|---|---|---|---|---|---|
| Homepage | 31 | 8.4s | 0.28 | 640ms | 11.2MB |
| Product Page | 28 | 6.9s | 0.19 | 520ms | 7.8MB |
| Collection Page | 34 | 5.2s | 0.14 | 410ms | 5.9MB |
Desktop scores were substantially better: Homepage 71, Product Page 68, Collection Page 74. This is typical - the desktop simulation does not expose the mobile-specific problems that real visitors experience. The team's stated goal was "getting above 50 on mobile." After reviewing the audit results, the revised target became 75+ on mobile with all Core Web Vitals in the Good range.
Phase 1: Full Store Audit
The audit took approximately four hours and used five tools in combination. No fixes were made during this phase - only documentation.
Baseline scores and diagnostic list. Run three times on each page type. Results documented in a spreadsheet.
Identified every external domain loading scripts, every image size and format, total page weight, and request count.
Identified unused JavaScript and CSS percentages by file. Files above 60 percent unused were flagged for conditional loading or removal.
Showed exactly when content appeared during loading. The homepage took 3.8 seconds to show anything visible and 12 seconds to fully stabilize.
Showed Liquid rendering time by section. The homepage navigation section was rendering in 340ms - far above the acceptable 50ms threshold.
What the audit found - 18 installed apps:
| App | Script Size | Pages Loaded | Monthly Cost | Last Used |
|---|---|---|---|---|
| Review app (Yotpo) | 142KB | All pages | $19 | Daily |
| Loyalty program | 88KB | All pages | $49 | Weekly |
| Upsell app | 74KB | All pages | $29 | Product only |
| Size guide | 62KB | All pages | $9 | Product only |
| Countdown timer | 38KB | All pages | $7 | Homepage only |
| Email popup | 55KB | All pages | $15 | Homepage only |
| Currency converter | 44KB | All pages | $0 | All pages |
| Social proof ticker | 47KB | All pages | $19 | Unknown |
| Wishlist app | 71KB | All pages | $9 | Product + collection |
| Back-in-stock app | 39KB | All pages | $19 | Product only |
| Instagram feed | 95KB | All pages | $9 | Homepage only |
| Chat widget | 68KB | All pages | $10 | All pages |
| Subscription app | 83KB | All pages | $39 | Product only |
| Page builder | 124KB | All pages | $39 | Landing pages |
| A/B testing tool | 77KB | All pages | $29 | Unknown |
| Cross-sell app | 58KB | All pages | $19 | Cart only |
| Gift card app | 31KB | All pages | $0 | Checkout |
| SEO booster | 29KB | All pages | $9 | Unknown |
Additional audit findings: average product image size 1.8MB (JPEG, original camera resolution), hero slider loading 5 images at 2.4MB each (12MB total on every homepage visit), 9 external domains loading scripts, and GA4 firing twice on every page (loaded both directly in theme.liquid and inside the GTM container).
Phase 2: Prioritized Fix Plan
Based on the audit, fixes were ranked by estimated score impact across three weeks of implementation organized into four phases.
Step 1: Image Optimization (Days 3 to 5)
All product images were replaced using a batch conversion script with Sharp (Node.js) to convert to WebP at 78 percent quality and resize to 1200px maximum width.
const sharp = require('sharp');
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(1200, null, { withoutEnlargement: true, fit: 'inside' })
.webp({ quality: 78 })
.toFile(outputPath);
}
<img
src="{{ section.settings.hero_image | image_url: width: 1400, format: 'webp' }}"
srcset="
{{ section.settings.hero_image | image_url: width: 750, format: 'webp' }} 750w,
{{ section.settings.hero_image | image_url: width: 1400, format: 'webp' }} 1400w
"
sizes="(max-width: 768px) 100vw, 100vw"
fetchpriority="high"
loading="eager"
width="1400"
height="600"
alt="{{ section.settings.hero_image.alt | escape }}"
>
Collection page pagination was reduced from 48 products to 24. Lazy loading was added to all product images after the first row.
Results after Step 1 (Day 5):
| Page | Before | After | Change |
|---|---|---|---|
| Homepage mobile score | 31 | 49 | +18 points |
| Homepage LCP | 8.4s | 4.1s | -4.3s |
| Homepage page weight | 11.2MB | 1.8MB | -9.4MB |
| Product Page mobile score | 28 | 43 | +15 points |
| Collection Page mobile score | 34 | 52 | +18 points |
Step 2: App Audit and Script Restriction (Days 8 to 13)
Three questions were applied to each app: Is it generating measurable revenue? Can it be page-restricted rather than global? Is there a lighter alternative?
Removed entirely (7 apps, saving 405KB and $87/month):
Page-restricted using Liquid conditionals (6 apps):
{% if template == 'product' %}
{% render 'size-guide-init' %}
{% render 'subscription-app-init' %}
{% render 'wishlist-init' %}
{% endif %}
{% if template == 'cart' %}
{% render 'upsell-init' %}
{% endif %}
{% if template == 'index' %}
{% render 'instagram-init' %}
{% render 'countdown-init' %}
{% render 'popup-init' %}
{% endif %}
Deferred until first interaction (3 apps): Chat widget, heatmap tool, and Klaviyo delayed until first scroll interaction or 4 seconds after load, whichever came first.
var deferredLoaded = false;
function loadDeferredApps() {
if (deferredLoaded) return;
deferredLoaded = true;
// Load chat widget and heatmap here
}
['scroll', 'mousemove', 'touchstart'].forEach(function(e) {
document.addEventListener(e, loadDeferredApps, { once: true, passive: true });
});
setTimeout(loadDeferredApps, 4000);
Tracking consolidation: GA4 was loading both directly and through GTM - the direct snippet was removed. All tracking (GA4, Facebook Pixel, TikTok Pixel, Pinterest Tag) now runs through GTM. External tracking domains reduced from 9 to 3.
Results after Step 2 (Day 13):
| Metric | Before | After | Change |
|---|---|---|---|
| Homepage mobile score | 49 | 61 | +12 points |
| Homepage TBT | 2,840ms | 980ms | -1,860ms |
| Homepage external domains | 9 | 3 | -6 domains |
| Homepage JS weight | 1,234KB | 387KB | -847KB |
| Product Page mobile score | 43 | 57 | +14 points |
| Collection Page mobile score | 52 | 64 | +12 points |
Step 3: Liquid Template Cleanup (Days 14 to 15)
The mega menu was building by looping all 40+ collections and making metafield lookups on each iteration - 340ms render time on every page of the store.
<!-- Before: inefficient collection loop with metafield lookups -->
{% for collection in collections %}
{% assign nav_image = collection.metafields.custom.nav_image %}
{% if nav_image != blank %}
<a href="{{ collection.url }}">
<img src="{{ nav_image | image_url: width: 200 }}" alt="{{ collection.title }}">
</a>
{% endif %}
{% endfor %}
<!-- After: using linklist, no full collection iteration -->
{% for link in linklists['main-menu'].links %}
{% if link.type == 'collection_link' %}
<a href="{{ link.url }}">{{ link.title }}</a>
{% endif %}
{% endfor %}
Navigation render time after: 28ms (down from 340ms). Six featured collection sections were each making independent collection queries - refactored to use shared section settings, eliminating 5 redundant queries per page load.
Results after Step 3 (Day 15):
| Metric | Before | After | Change |
|---|---|---|---|
| Homepage TTFB | 680ms | 210ms | -470ms |
| Product Page TTFB | 590ms | 180ms | -410ms |
| Homepage mobile score | 61 | 69 | +8 points |
| Product Page mobile score | 57 | 65 | +8 points |
Step 4: Advanced Resource Prioritization (Days 16 to 18)
Critical CSS extracted and inlined (14KB inlined, 128KB full stylesheet async):
<head>
<style>/* 14KB of above-fold critical CSS */</style>
<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>
</head>
LCP image preload with srcset-aware hint:
<link
rel="preload"
as="image"
href="{{ section.settings.hero_image | image_url: width: 1400, format: 'webp' }}"
imagesrcset="
{{ section.settings.hero_image | image_url: width: 750, format: 'webp' }} 750w,
{{ section.settings.hero_image | image_url: width: 1400, format: 'webp' }} 1400w
"
imagesizes="100vw"
>
Additional optimizations applied: Preconnects to Shopify CDN and Google Fonts, font weights reduced from 10 to 4 (2 weights each for Playfair Display and Lato), font-display: swap added with preload, and { passive: true } added to 14 scroll and touch event listeners in theme.js.
Results after Step 4 (Day 18):
| Page | Before | After | Change |
|---|---|---|---|
| Homepage mobile score | 69 | 79 | +10 points |
| Homepage LCP | 3.1s | 1.8s | -1.3s |
| Homepage FCP | 2.8s | 1.2s | -1.6s |
| Homepage CLS | 0.28 | 0.04 | -0.24 |
| Product Page mobile score | 65 | 74 | +9 points |
| Collection Page mobile score | 68 | 76 | +8 points |
Full Before vs After Comparison
Mobile PageSpeed scores:
| Page | Before | After | Improvement |
|---|---|---|---|
| Homepage | 31 | 79 | +48 points |
| Product Page | 28 | 74 | +46 points |
| Collection Page | 34 | 76 | +42 points |
Core Web Vitals:
| Metric | Before | After | Status |
|---|---|---|---|
| LCP | 8.4s (Poor) | 1.8s (Good) | Resolved |
| CLS | 0.28 (Poor) | 0.04 (Good) | Resolved |
| INP | 640ms (Poor) | 148ms (Good) | Resolved |
| TTFB | 680ms | 210ms | Significantly improved |
Technical metrics:
| Metric | Before | After | Change |
|---|---|---|---|
| Homepage page weight | 11.2MB | 1.4MB | -87% |
| Homepage JS weight | 1,234KB | 312KB | -75% |
| Homepage image weight | 9.8MB | 820KB | -92% |
| Homepage request count | 142 | 54 | -62% |
| External domains | 9 | 3 | -67% |
| Installed apps | 18 | 11 | -7 apps |
| Monthly app spend | $329 | $242 | -$87/month |
Business metrics (30 days post-optimization vs 30 days prior):
| Metric | Before | After | Change |
|---|---|---|---|
| Mobile conversion rate | 1.2% | 1.9% | +58% |
| Mobile bounce rate | 68% | 52% | -16 points |
| Mobile sessions per user | 1.4 | 1.8 | +29% |
| Revenue from mobile | $41,200 | $68,100 | +65% |
Challenges Faced
The Page Builder Lock-In
Two landing pages built entirely in the page builder required 6 hours to rebuild in native Shopify sections before the app could be removed. Lesson: page builders create vendor lock-in. Future landing pages were built in native sections from the start.
The Stakeholder Communication Failure
The marketing team had not been informed before the A/B testing app was removed. They discovered it when trying to run a new experiment, creating a same-day reinstall that undid the 77KB savings for two weeks. Lesson: communicate all app changes to stakeholders before implementing, not after.
Critical CSS Drift
Two weeks after implementation, a new homepage section was added without updating the inlined critical CSS. The section rendered without styles for 2 seconds before the full stylesheet loaded. Fix: critical CSS extraction was added as a required step in the deployment checklist for any new above-fold section.
Klaviyo Deferral Broke a Hero Form
Delaying Klaviyo until first scroll broke an email capture form in the hero section. Visitors who submitted before scrolling received no confirmation and were not added to the list. Fix: the hero form was moved to load Klaviyo on page load while all other Klaviyo functionality remained deferred. Lesson: test every form and interaction that depends on third-party scripts after deferral implementation.
Lessons Learned
The Audit Is Worth More Than the Fixes
Knowing exactly what caused the problems before touching anything saved significant time. The impulse to install a speed optimization app at the start would have made things worse - the audit revealed that app scripts were the primary problem.
Mobile Conversion Rate Is the Right Success Metric
Framing around conversion rate changed prioritization. The slider removal became a clear priority because it improved both performance and conversion simultaneously. PageSpeed score is a proxy - conversion rate is the outcome.
App Stack Decisions Are Business Decisions
Removing apps required conversations across marketing, paid social, and customer success. Technical decisions with business implications need stakeholder input. The A/B testing incident happened specifically because this principle was violated.
Liquid Problems Compound Across Every Page Load
The navigation loop was adding 340ms to every page on the store. Fixing one Liquid pattern affected 85,000 monthly sessions - the cumulative impact exceeded most frontend optimizations combined.
Replication Strategy
For stores with a similar profile (mid-size catalog, 3+ years of app accumulation, premium theme, multiple tracking pixels), this sequence applies directly.
Week 1: Foundation
- Days 1 to 2: Complete the full audit. Document every app, script size, page weight, and external domain. Do not fix anything until the audit is complete.
- Days 3 to 5: Image conversion to WebP, resize to display width, implement lazy loading with correct dimensions. Start with hero and homepage images.
- Days 6 to 7: Apply explicit width and height to every img tag. Implement lazy loading across collection pages.
Week 2: Scripts and Apps
- Day 8: App audit meeting with all stakeholders. Agree on removals and restrictions before making changes.
- Days 9 to 10: Remove approved apps. Test PageSpeed after each removal individually.
- Days 11 to 12: Implement page-specific Liquid conditionals for remaining apps. Test all restricted app functionality on their target pages.
- Day 13: GTM consolidation. Remove duplicate tracking implementations. Set specific triggers per tag.
Week 3: Advanced Optimization
- Day 14: Liquid audit using Theme Inspector. Fix the highest-TTFB Liquid issue first.
- Day 15: Font optimization - limit weights, add font-display swap, add preload.
- Day 16: LCP image preload with imagesrcset and fetchpriority.
- Day 17: Critical CSS inline, async full stylesheet load, preconnects.
- Day 18: Passive event listeners on all scroll and touch handlers.
Week 4: Verification
- Three PageSpeed tests per page type.
- Real device test on mid-range Android over 4G.
- Search Console Core Web Vitals check after 28 days.
- Set up Lighthouse CI before every future theme publish.
Summary
A mobile PageSpeed score improvement from 31 to 79, all Core Web Vitals moving from Poor to Good, 87 percent reduction in page weight, and $26,900 additional monthly mobile revenue. Achieved in 18 days, 46 developer hours, with a net reduction in monthly app spend.
The three changes that drove the most score improvement: image optimization and slider replacement (+18 points), app restriction and TBT reduction (+12 to 14 points per page), and the Liquid navigation fix combined with critical CSS (+8 to 10 points per page). Everything else - fonts, preloads, passive listeners, preconnects - contributed the final 10 points that pushed scores from the mid-60s into the upper 70s.
For stores looking to systematically work through each optimization layer with Shopify-native tooling, Ecom: Page Speed Expert handles image optimization, script management, and performance monitoring - covering the image and script layers that account for the largest portion of score gains in this case study. The work is available to anyone willing to do it systematically.