Shopify Performance Monitoring: Automated Alerts for Store Speed

· 7 min read · Ecommerce

Build an automated monitoring system that tracks Shopify store performance, detects speed regressions, and sends alerts before customers notice — using Lighthouse CI, synthetic checks, and threshold-based notifications.

Shopify Performance Monitoring: Automated Alerts for Store Speed

A Shopify store that loads in 2 seconds today might load in 6 seconds next week. A new app gets installed. A theme update ships a larger JavaScript bundle. Someone adds an unoptimised hero image. Nobody notices until conversion rates drop and by then you have lost a week of revenue.

Automated performance monitoring catches these regressions the day they happen. Run synthetic speed checks on a schedule, compare against baselines, and get alerts before customers start bouncing.

This guide builds the complete monitoring pipeline: measurement, storage, trend analysis, and alerting.

Who This Is For

  • Store owners who have optimised their Shopify speed once and want to keep it that way
  • Ecommerce agencies managing multiple client stores who need automated oversight
  • Vibe coders building performance monitoring tools for ecommerce clients
  • Developers who want to catch performance regressions before they ship to production

Basic Python knowledge and a Shopify store URL are all you need to start.

The Monitoring Architecture

What You Will Need

pip install requests pandas sqlalchemy jinja2
  • Google PageSpeed Insights API key (free, 25,000 queries/day)
  • Your Shopify store URLs (homepage, collection page, product page)

Step 1: Build the Speed Measurement Client

Use Google’s PageSpeed Insights API to measure Core Web Vitals and Lighthouse scores.

import requests
import pandas as pd
from datetime import datetime
from sqlalchemy import create_engine

class SpeedMonitor:
    """Measure and track Shopify store page speed."""

    PSI_URL = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"

    def __init__(self, api_key: str, db_url: str = "sqlite:///speed_monitor.db"):
        """Initialise with PSI API key and database."""
        self.api_key = api_key
        self.engine = create_engine(db_url)
        print("Speed monitor initialised")

    def measure_page(self, url: str, strategy: str = "mobile") -> dict:
        """Run a PageSpeed Insights measurement on a single URL."""
        params = {
            "url": url,
            "key": self.api_key,
            "strategy": strategy,
            "category": "performance",
        }

        resp = requests.get(self.PSI_URL, params=params)
        resp.raise_for_status()
        data = resp.json()

        # Extract Lighthouse scores
        lighthouse = data.get("lighthouseResult", {})
        audits = lighthouse.get("audits", {})
        categories = lighthouse.get("categories", {})

        # Core Web Vitals
        metrics = {
            "url": url,
            "strategy": strategy,
            "measured_at": datetime.now().isoformat(),
            "performance_score": int(categories.get("performance", {}).get("score", 0) * 100),
            "fcp_ms": audits.get("first-contentful-paint", {}).get("numericValue", 0),
            "lcp_ms": audits.get("largest-contentful-paint", {}).get("numericValue", 0),
            "cls": audits.get("cumulative-layout-shift", {}).get("numericValue", 0),
            "tbt_ms": audits.get("total-blocking-time", {}).get("numericValue", 0),
            "speed_index_ms": audits.get("speed-index", {}).get("numericValue", 0),
            "tti_ms": audits.get("interactive", {}).get("numericValue", 0),
            "total_byte_weight": audits.get("total-byte-weight", {}).get("numericValue", 0),
            "dom_size": audits.get("dom-size", {}).get("numericValue", 0),
            "third_party_count": len(audits.get("third-party-summary", {}).get("details", {}).get("items", [])),
        }

        print(f"  {url}: score={metrics['performance_score']}, LCP={metrics['lcp_ms']:.0f}ms")
        return metrics

Step 2: Define Pages to Monitor

Monitor the pages that matter most: homepage, top collection, top product, cart.

class StoreMonitor:
    """Monitor multiple pages across a Shopify store."""

    def __init__(self, store_url: str, api_key: str, db_url: str = "sqlite:///speed_monitor.db"):
        """Initialise with store URL and monitoring config."""
        self.store_url = store_url.rstrip("/")
        self.speed = SpeedMonitor(api_key, db_url)
        self.engine = self.speed.engine

        # Default pages to monitor
        self.pages = [
            {"path": "/", "label": "Homepage"},
            {"path": "/collections/all", "label": "Collection"},
            {"path": "/collections", "label": "Collections Index"},
        ]

    def add_page(self, path: str, label: str):
        """Add a page to the monitoring list."""
        self.pages.append({"path": path, "label": label})

    def run_full_check(self) -> pd.DataFrame:
        """Measure all monitored pages."""
        results = []
        print(f"Running speed check on {len(self.pages)} pages...")

        for page in self.pages:
            url = f"{self.store_url}{page['path']}"
            try:
                # Mobile measurement (primary)
                mobile = self.speed.measure_page(url, strategy="mobile")
                mobile["page_label"] = page["label"]
                results.append(mobile)

                # Desktop measurement
                desktop = self.speed.measure_page(url, strategy="desktop")
                desktop["page_label"] = page["label"]
                results.append(desktop)

            except Exception as e:
                print(f"  Error measuring {url}: {e}")
                results.append({
                    "url": url,
                    "page_label": page["label"],
                    "strategy": "error",
                    "measured_at": datetime.now().isoformat(),
                    "performance_score": 0,
                    "error": str(e),
                })

        df = pd.DataFrame(results)
        df.to_sql("speed_measurements", self.engine, if_exists="append", index=False)
        print(f"Stored {len(df)} measurements")
        return df

Step 3: Establish Performance Baselines

A regression needs a baseline. Calculate rolling averages to compare against.

class BaselineCalculator:
    """Calculate and maintain performance baselines."""

    def __init__(self, engine):
        """Initialise with database engine."""
        self.engine = engine

    def calculate_baselines(self, days: int = 14) -> pd.DataFrame:
        """Calculate baseline metrics from recent measurements."""
        query = f"""
            SELECT
                url,
                page_label,
                strategy,
                AVG(performance_score) as baseline_score,
                AVG(lcp_ms) as baseline_lcp,
                AVG(fcp_ms) as baseline_fcp,
                AVG(cls) as baseline_cls,
                AVG(tbt_ms) as baseline_tbt,
                AVG(speed_index_ms) as baseline_speed_index,
                PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY lcp_ms) as p75_lcp,
                PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY fcp_ms) as p75_fcp,
                COUNT(*) as measurement_count
            FROM speed_measurements
            WHERE measured_at >= datetime('now', '-{days} days')
              AND strategy != 'error'
            GROUP BY url, page_label, strategy
        """
        df = pd.read_sql(query, self.engine)
        df.to_sql("performance_baselines", self.engine, if_exists="replace", index=False)
        print(f"Baselines calculated from {days} days of data")
        return df

    def detect_regressions(self, current: pd.DataFrame) -> list[dict]:
        """Compare current measurements against baselines."""
        baselines = pd.read_sql("SELECT * FROM performance_baselines", self.engine)
        regressions = []

        for _, measurement in current.iterrows():
            baseline = baselines[
                (baselines["url"] == measurement["url"]) &
                (baselines["strategy"] == measurement["strategy"])
            ]

            if len(baseline) == 0:
                continue

            b = baseline.iloc[0]

            # Score regression (drop of 10+ points)
            score_diff = measurement["performance_score"] - b["baseline_score"]
            if score_diff <= -10:
                regressions.append({
                    "url": measurement["url"],
                    "page_label": measurement.get("page_label", ""),
                    "strategy": measurement["strategy"],
                    "metric": "performance_score",
                    "current": measurement["performance_score"],
                    "baseline": round(b["baseline_score"], 1),
                    "change": round(score_diff, 1),
                    "severity": "critical" if score_diff <= -20 else "warning",
                })

            # LCP regression (increase of 500ms+)
            lcp_diff = measurement["lcp_ms"] - b["baseline_lcp"]
            if lcp_diff >= 500:
                regressions.append({
                    "url": measurement["url"],
                    "page_label": measurement.get("page_label", ""),
                    "strategy": measurement["strategy"],
                    "metric": "lcp_ms",
                    "current": round(measurement["lcp_ms"]),
                    "baseline": round(b["baseline_lcp"]),
                    "change": round(lcp_diff),
                    "severity": "critical" if lcp_diff >= 1000 else "warning",
                })

            # CLS regression (increase of 0.05+)
            cls_diff = measurement["cls"] - b["baseline_cls"]
            if cls_diff >= 0.05:
                regressions.append({
                    "url": measurement["url"],
                    "page_label": measurement.get("page_label", ""),
                    "strategy": measurement["strategy"],
                    "metric": "cls",
                    "current": round(measurement["cls"], 3),
                    "baseline": round(b["baseline_cls"], 3),
                    "change": round(cls_diff, 3),
                    "severity": "critical" if cls_diff >= 0.1 else "warning",
                })

        print(f"Detected {len(regressions)} regressions")
        return regressions

Step 4: Build the Alert Engine

Send actionable alerts when regressions are detected.

import json
import os

class PerformanceAlertEngine:
    """Send performance regression alerts via Slack or email."""

    def __init__(self, slack_webhook: str = None):
        """Initialise with notification channels."""
        self.slack_webhook = slack_webhook or os.environ.get("SLACK_WEBHOOK_URL")

    def send_regression_alert(self, regressions: list[dict], store_url: str):
        """Send a formatted regression alert."""
        if not regressions:
            print("No regressions — no alert needed")
            return

        critical = [r for r in regressions if r["severity"] == "critical"]
        warnings = [r for r in regressions if r["severity"] == "warning"]

        # Build message
        lines = [f"*Performance Alert: {store_url}*"]
        lines.append(f"Detected {len(regressions)} regressions ({len(critical)} critical)")
        lines.append("")

        if critical:
            lines.append("*Critical:*")
            for r in critical:
                lines.append(f"• {r['page_label']} ({r['strategy']}): "
                             f"{r['metric']} changed from {r['baseline']}{r['current']} "
                             f"({r['change']:+})")

        if warnings:
            lines.append("*Warnings:*")
            for r in warnings:
                lines.append(f"• {r['page_label']} ({r['strategy']}): "
                             f"{r['metric']} changed from {r['baseline']}{r['current']} "
                             f"({r['change']:+})")

        message = "\n".join(lines)

        # Send to Slack
        if self.slack_webhook:
            requests.post(self.slack_webhook, json={"text": message})
            print("Slack alert sent")
        else:
            print("No Slack webhook configured")
            print(message)

    def generate_report(self, measurements: pd.DataFrame, regressions: list[dict]) -> str:
        """Generate an HTML performance report."""
        summary = measurements.groupby(["page_label", "strategy"]).agg(
            score=("performance_score", "mean"),
            lcp=("lcp_ms", "mean"),
            cls=("cls", "mean"),
        ).round(1).reset_index()

        report = f"""<h2>Performance Report — {datetime.now().strftime('%Y-%m-%d')}</h2>
<table>
<tr><th>Page</th><th>Device</th><th>Score</th><th>LCP (ms)</th><th>CLS</th></tr>
"""
        for _, row in summary.iterrows():
            score_colour = "#22c55e" if row["score"] >= 90 else "#f59e0b" if row["score"] >= 50 else "#ef4444"
            report += f"""<tr>
<td>{row['page_label']}</td>
<td>{row['strategy']}</td>
<td style="color:{score_colour}">{row['score']}</td>
<td>{row['lcp']:.0f}</td>
<td>{row['cls']:.3f}</td>
</tr>"""
        report += "</table>"

        if regressions:
            report += f"<p><strong>{len(regressions)} regressions detected</strong></p>"

        return report

Step 5: Schedule the Monitor

Run checks twice daily and send alerts when thresholds are breached.

from prefect import flow

@flow(name="shopify-performance-monitor")
def monitor_store_performance(
    store_url: str,
    psi_api_key: str,
    db_url: str = "sqlite:///speed_monitor.db",
):
    """Daily performance monitoring pipeline."""
    # Measure
    monitor = StoreMonitor(store_url, psi_api_key, db_url)
    measurements = monitor.run_full_check()

    # Analyse
    from sqlalchemy import create_engine
    engine = create_engine(db_url)
    baselines = BaselineCalculator(engine)
    baselines.calculate_baselines(days=14)
    regressions = baselines.detect_regressions(measurements)

    # Alert
    alerter = PerformanceAlertEngine()
    alerter.send_regression_alert(regressions, store_url)

    # Report
    report = alerter.generate_report(measurements, regressions)

    print(f"Monitoring complete: {len(measurements)} measurements, {len(regressions)} regressions")
    return {"measurements": len(measurements), "regressions": len(regressions)}


# Schedule twice daily
monitor_store_performance.serve(
    name="speed-monitor-twice-daily",
    schedules=[
        {"cron": "0 6 * * *", "timezone": "Europe/London"},
        {"cron": "0 18 * * *", "timezone": "Europe/London"},
    ],
)

What This Replaces

Manual ApproachAutomated Monitoring
Run Lighthouse manually every few weeksAutomated daily measurements
No historical data — each test is isolatedFull history with trend analysis
Notice speed issues after conversion dropsAlerts on the day regression happens
Test homepage onlyMulti-page monitoring with baselines
No threshold configurationConfigurable alert rules per metric
No team visibility into store healthShared reports and Slack alerts

Next Steps

For the complete guide to diagnosing and fixing Shopify performance issues, see Why Is My Shopify Store Slow? Diagnose and Fix Performance. For fixing the most common Shopify speed problems, see How to Fix Slow Shopify Stores.

For building a self-updating dashboard from the monitoring data, see Shopify Automated Reporting. For scheduling the monitoring pipeline with Prefect, see Schedule and Orchestrate Workflows with Prefect.

Ecommerce optimisation services include performance monitoring setup, Core Web Vitals optimisation, and ongoing store speed management.

Get in touch to discuss setting up automated performance monitoring for your store.

shopify performance monitoring shopify performance issues shopify speed monitoring shopify store speed alerts shopify lighthouse monitoring shopify performance alerts monitor shopify store speed shopify core web vitals shopify performance regression automated shopify performance testing

Enjoyed this article?

Get notified when I publish new articles on automation, ecommerce, and data engineering.

Get in touch

Related Articles