Commercial performance monitoring starts at $29-99/month.
With Google PageSpeed API + GitHub Actions, you get continuous monitoring at $0.
The Need
After launching a site, you need to continuously monitor Core Web Vitals:
- LCP (Largest Contentful Paint)
- CLS (Cumulative Layout Shift)
- INP (Interaction to Next Paint)
Commercial options: Speedcurve, Calibre, Sentry Performance — starting at $29-99/month.
Free option: Build it yourself.
Architecture
GitHub Actions (cron daily)
→ Call PageSpeed Insights API
→ Parse LCP/CLS/INP scores
→ Store as GitHub Artifact (historical comparison)
→ Score below threshold → Alert via PR/Issue
PageSpeed API Free Tier
- QPS: 20 requests/second
- Daily quota: ~1,250 push triggers + daily cron is more than enough
- API Key: Optional (higher limits with key)
- Free tier: More than sufficient for personal projects
Implementation
Step 1: Base Workflow
# .github/workflows/pagespeed.yml
name: PageSpeed Insights
on:
schedule:
- cron: '0 0 * * *' # Daily at UTC 00:00
push:
branches: [main]
jobs:
pagespeed:
runs-on: ubuntu-latest
steps:
- name: Run PageSpeed
run: |
URL="https://utlkit.com"
API_KEY="${{ secrets.PAGESPEED_API_KEY }}"
curl -s "https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${URL}&category=PERFORMANCE&category=ACCESSIBILITY&strategy=mobile${API_KEY:+&key=${API_KEY}}" \
-o pagespeed-mobile.json
curl -s "https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${URL}&category=PERFORMANCE&category=ACCESSIBILITY&strategy=desktop${API_KEY:+&key=${API_KEY}}" \
-o pagespeed-desktop.json
- name: Extract scores
run: |
echo "Mobile Performance: $(jq '.categories.performance.score' pagespeed-mobile.json)"
echo "Mobile Accessibility: $(jq '.categories.accessibility.score' pagespeed-mobile.json)"
echo "Desktop Performance: $(jq '.categories.performance.score' pagespeed-desktop.json)"
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: pagespeed-${{ github.sha }}
path: pagespeed-*.json
retention-days: 90
Step 2: Add Retry Logic
PageSpeed API occasionally returns 429 (rate limited) or 500:
MAX_RETRIES=3
RETRY_DELAY=5
for i in $(seq 1 $MAX_RETRIES); do
HTTP_CODE=$(curl -s -o response.json -w "%{http_code}" \
"https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${URL}&strategy=${STRATEGY}${API_KEY:+&key=${API_KEY}}")
if [ "$HTTP_CODE" = "200" ]; then exit 0; fi
if [ "$HTTP_CODE" = "429" ]; then
echo "Rate limited, waiting ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
RETRY_DELAY=$((RETRY_DELAY * 2)) # Exponential backoff
else
sleep $RETRY_DELAY
fi
done
echo "Failed after $MAX_RETRIES retries"
exit 1
Step 3: Add Alerts
LCP=$(jq '.audits.largest-contentful-paint.score' pagespeed-mobile.json)
CLS=$(jq '.audits.cumulative-layout-shift.score' pagespeed-mobile.json)
INP=$(jq '.audits.interaction-to-next-paint.score' pagespeed-mobile.json)
# Threshold alert (Google recommends: ≥90 good, ≥50 needs improvement)
if [ "$LCP" -lt 50 ] || [ "$CLS" -lt 50 ] || [ "$INP" -lt 50 ]; then
echo "::warning title=Core Web Vitals below threshold::LCP=$LCP, CLS=$CLS, INP=$INP"
fi
Step 4: CrUX Dashboard Integration
Chrome UX Report (CrUX) provides real user performance data:
- Visit https://chromestatus.com/speed
- Enter your domain
- View real-world LCP/CLS/INP distributions
This data is more authentic than PSI's lab data — it reflects global Chrome users' actual experience.
Real Debugging Case: CLS Spike
PageSpeed CI alert: CLS jumped from 0.01 to 0.25.
Debugging:
- PSI details → CLS contributor is the ad slot
- Ad slot loads dynamically with unknown height → layout shift
- Fix: Set
aspect-ratioon ad container to reserve space - Redeploy → CI runs PSI → CLS back to 0.01
Without CI, this regression wouldn't be found until users complained.
Cost Comparison
| Option | Monthly Cost | Capability |
|---|---|---|
| Speedcurve | $99+ | Professional |
| Calibre | $39+ | Professional |
| PageSpeed CI (this approach) | $0 | More than enough |
Lessons Learned
- Configure API Key — Free tier is enough, but a key is more stable
- Always add retries — PageSpeed API occasionally 429s
- Keep Artifacts for 90 days — Enough to track performance trends
- CrUX for real data — PSI is lab data; CrUX is real users
- Set alert threshold at 50 — Google's recommended line; too low = false alarms, too high = useless
Project
utlkit.com — 150+ free online tools
Top comments (0)