A simple Node.js tool to test and validate Core Web Vitals (CLS, LCP, FID) and performance scores using Google's PageSpeed Insights API.
Perfect for: Fixing CLS issues, optimizing LCP, improving FID, monitoring performance scores, validating Core Web Vitals compliance, working with Cursor AI to fix web performance issues.
Key Feature: Validation mode runs tests 3 times at 45-second intervals to ensure your fix is consistently good, not just a lucky one-time result.
What It Tests:
- ✅ CLS (Cumulative Layout Shift) - Layout stability
- ✅ LCP (Largest Contentful Paint) - Loading performance
- ✅ FID (First Input Delay) - Interactivity
- ✅ Performance Score - Overall page performance (0-100)
- ✅ Plus: FCP, SI, TBT, TTI
Get your free API key: https://developers.google.com/speed/docs/insights/v5/get-started
cd ~/Documents/repositories/psi-testing-tool
# Copy the example config
cp config.json.example config.json
# Edit and add your API key
nano config.jsonReplace YOUR_GOOGLE_API_KEY_HERE with your actual key:
{
"apiKey": "YOUR_ACTUAL_API_KEY_HERE",
"thresholds": { ... },
...
}Note: config.json is gitignored - your API key stays private!
npm startTest any URL instantly without editing files:
# Single URL (mobile by default)
node index.js https://your-site.com/page
# Multiple URLs
node index.js https://site1.com https://site2.com https://site3.com
# Test desktop instead of mobile
node index.js --desktop https://your-site.com/page
# Test BOTH mobile and desktop
node index.js --both https://your-site.com/pageBest for: One-off tests, quick checks during development
Create urls.txt with one URL per line:
cp urls.txt.example urls.txt
nano urls.txtExample urls.txt:
# Production URLs
https://mysite.com/homepage
https://mysite.com/product-page
https://mysite.com/checkout
# Comments start with #
Then run:
# Test mobile (default)
npm start
# Test desktop
npm start -- --desktop
# Test BOTH mobile and desktop
npm start -- --bothBest for: Testing multiple pages, ongoing monitoring, non-technical users
Priority: Command-line URLs > urls.txt
- No flag = Mobile (default)
--mobile= Mobile (explicit)--desktop= Desktop only--both= Test each URL on both mobile AND desktop
When using --both, each URL is tested twice (mobile first, then desktop).
Problem: PSI scores fluctuate. One test might show CLS 0.08 (good), next shows 0.12 (bad).
Solution: Run 3 tests to ensure consistency across all metrics.
npm run validateWhat it does:
- Tests each URL
- Waits 45 seconds
- Tests again
- Waits 45 seconds
- Tests a third time
- Shows all 3 results + average
- Exit code 0 if all pass ✅, exit code 1 if any fail ❌
Example output:
▶ RUN 1/3
Testing: Program Detail Page...
CLS: 0.027 ✅ GOOD
Performance: 100/100 ✅ GOOD
⏳ Waiting 45 seconds...
▶ RUN 2/3
CLS: 0.023 ✅ GOOD
▶ RUN 3/3
CLS: 0.031 ✅ GOOD
📊 VALIDATION SUMMARY
Average CLS: 0.027 (min: 0.023, max: 0.031)
✅ VALIDATION PASSED
Perfect for validating fixes before creating a PR!
This tool was designed to work seamlessly with Cursor AI for fixing any Core Web Vitals issue. Here's how:
Test your pages to find performance issues:
npm startNote which metric is failing (CLS, LCP, FID, or Performance Score) and any culprit elements.
Open your project in Cursor and provide this information:
Example 1: CLS Issue
I have a CLS issue on [PAGE_TYPE] pages.
PSI Results (Mobile):
- Current CLS: 0.213 (POOR) - threshold is 0.1
- Culprit element: <div class="hero-wrapper">
- URL: https://your-site.com/problem-page
Requirements:
- Keep changes minimal and focused
- Avoid changing global files unless necessary
- I'll validate with: npm run validate
Please:
1. Analyze why the layout shifts
2. Implement a fix
3. Explain what you changed
Example 2: LCP Issue
I have an LCP issue on [PAGE_TYPE] pages.
PSI Results (Mobile):
- Current LCP: 3.8s (POOR) - threshold is 2.5s
- Main element: Hero image loading slowly
- URL: https://your-site.com/problem-page
Please optimize image loading to improve LCP below 2.5s.
Example 3: Performance Score
Performance score is 65/100 on [PAGE_TYPE] pages.
Issues identified by PSI:
- LCP: 2.8s
- CLS: 0.15
- TBT: 450ms
- URL: https://your-site.com/problem-page
Please analyze and fix the main bottlenecks.
After Cursor implements the fix:
git add .
git commit -m "fix(CLS): reserve space for hero section"
git push origin your-branchWait for your platform to rebuild/deploy:
sleep 45 # Wait 45 seconds for deploymentcd ~/Documents/repositories/psi-testing-tool
npm run validate- ✅ All green (exit code 0) → Fix validated! Create your PR.
- ❌ Some red (exit code 1) → Needs more work. Go back to Cursor with new data.
If validation fails:
Validation failed. Results:
Run 1: CLS 0.089
Run 2: CLS 0.121 ❌
Run 3: CLS 0.095
PSI shows the culprit is now: <div class="hero-image">
Can you try a different approach? Maybe we need to reserve space earlier in the page load.
Repeat until all 3 runs pass consistently.
# Mobile (default)
node index.js https://staging.mysite.com/new-feature
# Desktop
node index.js --desktop https://staging.mysite.com/new-feature
# Both
node index.js --both https://staging.mysite.com/new-feature# Create urls.txt
echo "https://mysite.com/page1
https://mysite.com/page2
https://mysite.com/page3" > urls.txt
# Test mobile (default)
npm start
# Test both mobile and desktop
npm start -- --both# Before fix
node index.js https://your-site.com/page > before.txt
# After fix (after deployment)
node index.js https://your-site.com/page > after.txt
# Compare
diff before.txt after.txtnode index.js --detailed https://your-site.comShows which specific elements are causing layout shifts.
# Desktop only
npm start -- --desktop
# Or command-line
node index.js --desktop https://your-site.com# Each URL tested twice (mobile + desktop)
npm start -- --both
# With validation (3 runs × 2 strategies = 6 tests per URL)
npm run validate -- --bothnode index.js --watch https://staging.mysite.comTests every 60 seconds. Good for monitoring during active development.
Add to ~/.zshrc or ~/.bashrc:
alias psi='cd ~/Documents/repositories/psi-testing-tool && node index.js'
alias psiv='cd ~/Documents/repositories/psi-testing-tool && npm run validate'Usage:
psi https://mysite.com/page
psivEdit config.json to customize what's considered "good":
{
"thresholds": {
"cls": 0.1, // Good if < 0.1
"lcp": 2500, // Good if < 2500ms
"fid": 100, // Good if < 100ms
"performance": 90 // Good if >= 90
}
}Google's Web Vitals:
- CLS: < 0.1 = Good, 0.1-0.25 = Needs Improvement, > 0.25 = Poor
- LCP: < 2.5s = Good, 2.5-4s = Needs Improvement, > 4s = Poor
- Performance: >= 90 = Good, 50-89 = Average, < 50 = Poor
{
"validation": {
"runs": 3, // How many times to test each URL
"intervalSeconds": 45, // Wait time between test runs
"requireAllPass": true // Exit code 1 if any run fails
}
}Use validation mode when:
- Fixing CLS issues (scores can vary)
- Optimizing LCP (loading times fluctuate)
- Verifying performance improvements
- Before creating a PR
- In CI/CD pipelines
{
"options": {
"saveResults": true, // Save to results/ directory
"resultsDir": "./results", // Where to save
"verbose": false // Show extra debug info
}
}Solution: Run npm install first to set up npm scripts.
"ENOTFOUND www.googleapis.com"
Solution: You're in a restricted network. Try different WiFi or VPN.
Solution:
- Check your key in
config.json - Verify it's enabled for PageSpeed Insights API
- Regenerate at: https://console.cloud.google.com/apis/credentials
Solution: Add URLs using one of the three methods (command-line, urls.txt, or config.json).
✅ Good: https://example.com, http://localhost:3000
❌ Bad: example.com, www.example.com
This is normal for scores near the threshold. If average is good but variance is high, the fix might need refinement. Aim for all 3 runs to pass.
PSI only tests public URLs. Deploy to a staging/branch URL first, then test.
Google PSI free tier: 400 requests/minute, 25,000/day
Each validation uses 3 requests per URL. If you hit limits, wait a few minutes.
This tool helps you validate fixes for all Core Web Vitals:
- Reserve space for dynamic content
- Fix layout shifts from images, fonts, or injected content
- Validate that fixes work consistently
- Optimize image loading
- Improve server response times
- Validate loading performance improvements
- Optimize JavaScript execution
- Reduce blocking time
- Improve page interactivity
- Overall page optimization
- Best practices compliance
- Track improvements over time
With proper fixes and this validation workflow:
- CLS: 0.2+ reduced to < 0.1 (✅ passing)
- Performance: Improved to 90+ consistently
- Time to fix: 30-60 minutes with Cursor AI
- Validation: 3 consistent passing runs
# Single test (uses urls.txt, mobile by default)
npm start
# Test desktop
npm start -- --desktop
# Test both mobile and desktop
npm start -- --both
# Test specific URL
node index.js https://your-site.com/page
# Test multiple URLs from command line
node index.js https://site1.com https://site2.com
# Test desktop from command line
node index.js --desktop https://your-site.com
# Test both mobile and desktop from command line
node index.js --both https://your-site.com
# Validation mode (3 runs, 45s intervals)
npm run validate
# Validation with both mobile and desktop
npm run validate -- --both
# Detailed mode (shows culprit elements)
node index.js --detailed https://your-site.com
# Watch mode (continuous testing every 60s)
node index.js --watch https://your-site.com
# View saved results
ls -lt results/
cat results/psi-results-[timestamp].json# In your project root
mkdir -p tools
cp -r ~/Documents/repositories/psi-testing-tool tools/
# Add to git
echo "tools/psi-testing-tool/results/" >> .gitignore
echo "tools/psi-testing-tool/urls.txt" >> .gitignore
git add tools/psi-testing-tool
git commit -m "docs: add PSI testing tool for CLS validation"
git pushEach team member should:
- Clone/pull the repo
- Get their own Google API key
- Add it to
tools/psi-testing-tool/config.json - Start testing!
How it works:
- Reads URLs from command-line,
urls.txt, orconfig.json - Calls Google PageSpeed Insights API
- Google runs Lighthouse on their servers
- Parses CLS, LCP, Performance scores from response
- Applies thresholds and formats output
- Saves results to
results/directory
Dependencies: None! Uses only Node.js built-ins (https, fs, path)
Exit Codes:
0= All tests passed (good for CI/CD pipelines)1= Some tests failed or error occurred
API Limits: 25,000 requests/day (free tier)
Files Structure:
psi-testing-tool/
├── index.js # Main tool
├── package.json # NPM scripts
├── config.json.example # Config template (commit this)
├── config.json # Your config & API key (gitignored)
├── urls.txt.example # URL template (commit this)
├── urls.txt # Your URLs (gitignored)
├── README.md # This file
└── results/ # Auto-saved test results (gitignored)
Security: config.json and urls.txt are gitignored to protect your API key and private URLs.
Q: Why does it take so long?
A: Each PSI test runs a full Lighthouse audit on Google's servers. Takes 30-60 seconds per URL. Validation mode multiplies this by 3.
Q: Can I test localhost?
A: No, PSI only tests public URLs. Deploy to a staging URL first.
Q: What if scores fluctuate wildly?
A: That's why validation mode exists! Run 3 tests to get a reliable average. If variance is high, the page might have intermittent issues. This is especially common for CLS and LCP.
Q: Can I test desktop instead of mobile?
A: Yes, use --desktop flag: npm start -- --desktop or node index.js --desktop https://url.com
Q: Do I need to wait 45 seconds between tests?
A: Yes, for validation mode. This ensures Google's cache is refreshed and you get fresh measurements each time.
Q: Can I use this in CI/CD?
A: Yes! The tool exits with code 0 (pass) or 1 (fail), perfect for automated pipelines.
Made with ❤️ for Core Web Vitals optimization
Created: February 2026
Status: Production-ready ✅
Focus: CLS, LCP, FID, Performance Score
Need help? Open an issue or check the code - it's simple and well-commented!