Performance Metrics
The Performance endpoint provides Lighthouse-like performance measurements with detailed Core Web Vitals, resource analysis, and optimization recommendations. It measures real page load performance in controlled lab conditions, analyzes network resources, and provides actionable tips to improve page speed. Results are automatically cached for 24 hours to improve API performance.
Endpoint
GET /performanceQuery Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | The public URL to measure. Must be a valid HTTP/HTTPS URL. Cannot be localhost or private IPs (SSRF protection). |
| device | string | No | Device type for viewport emulation: mobile, tablet, or desktop (default: desktop). Affects viewport size, user agent, and performance characteristics. |
| waitUntil | string | No | Navigation wait condition: load, domcontentloaded, networkidle0, networkidle2 (default: networkidle2) |
| timeout | number | No | Navigation timeout in milliseconds. Must be between 1000ms (1 second) and 60000ms (60 seconds). Default: 30000 |
Wait Conditions
The waitUntil parameter determines when the page is considered fully loaded:
Waits for the load event (DOM + resources)
Use case: Fast measurements, minimal wait time
Waits for DOMContentLoaded event only
Use case: Very fast, but may miss lazy-loaded content
Waits until no network connections for 500ms
Use case: SPA with heavy async content
Waits until ≤2 network connections for 500ms
Use case: Balance between accuracy and speed
Core Web Vitals
The performance endpoint returns the three Core Web Vitals metrics that Google uses for ranking:
Largest Contentful Paint
Measures loading performance. Should occur within 2.5s of page start.
Interaction to Next Paint
Measures responsiveness. Pages should have INP of 200ms or less.
Cumulative Layout Shift
Measures visual stability. Should maintain CLS of 0.1 or less.
Example Request
curl "https://api.webpeek.dev/performance?url=https://stripe.com&device=mobile"Example Response
{
"schema_version": "1.0.0",
"run_id": "perf_2025-01-06T12-00-00_a1b2c3d4",
"url": "https://example.com/page",
"mode": "lab",
"environment": {
"device_profile": "desktop-4xCPU",
"network_profile": "no-throttling",
"collector_version": "webpeek-perf@1.0.0",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"page_context": {
"viewport": "1920x1080",
"timezone": "America/New_York",
"language": "en-US",
"cookies_enabled": true
}
},
"summary": {
"overall": "good",
"warnings": 1,
"errors": 0,
"suggestions": 3,
"main_issue": "Optimize images to reduce LCP"
},
"core_web_vitals": {
"lcp": {
"value_ms": 2800,
"rating": "needs-improvement",
"budget_ms": 2500,
"status": "warn"
},
"inp": {
"value_ms": 150,
"rating": "good",
"budget_ms": 200,
"status": "pass"
},
"cls": {
"value": 0.08,
"rating": "good",
"budget": 0.1,
"status": "pass"
},
"fcp": {
"value_ms": 1200,
"rating": "good",
"budget_ms": 1800,
"status": "pass"
},
"ttfb": {
"value_ms": 450,
"rating": "good",
"budget_ms": 800,
"status": "pass"
}
},
"performance_score": {
"schema_version": "1.0.0",
"rule_version": "2025.11.0",
"value": 78,
"scale": "0-100",
"method": "weighted-metrics",
"weights": {
"lcp": 0.35,
"inp": 0.35,
"cls": 0.15,
"fcp": 0.1,
"ttfb": 0.05
}
},
"resources": {
"total_requests": 45,
"transfer_kb": 2345.6,
"decoded_kb": 3456.7,
"first_party": {
"requests": 30,
"transfer_kb": 1500.2,
"decoded_kb": 2100.5
},
"third_party": {
"requests": 15,
"transfer_kb": 845.4,
"decoded_kb": 1356.2
},
"by_type": {
"javascript": {
"count": 12,
"transfer_kb": 890.1,
"decoded_kb": 2345.6,
"main_thread_ms": 1250
},
"image": {
"count": 18,
"transfer_kb": 1234.5,
"decoded_kb": 1456.8,
"unoptimized": 5
},
"stylesheet": {
"count": 5,
"transfer_kb": 123.4,
"decoded_kb": 456.7
}
},
"top_hosts": [
{
"host": "example.com",
"requests": 30,
"transfer_kb": 1500.2
}
],
"caching": {
"cacheable_requests": 40,
"missing_cache_headers": 5,
"cache_hit_ratio": 0.75,
"compression": {
"gzip_or_br": 38,
"uncompressed": 7,
"estimated_savings_kb": 567.8
}
}
},
"tips": [
{
"tip_id": "optimize-images",
"severity": "high",
"message": "Optimize images to reduce LCP",
"learn_more": "https://web.dev/optimize-lcp/"
}
],
"cache": {
"cached": false,
"cache_key": "perf:v1:https://example.com/page:desktop:networkidle2:30000",
"ttl_seconds": 86400,
"expires_at": "2025-01-07T12:00:00.000Z"
},
"created_at": "2025-01-06T12:00:00.000Z",
"metadata": {
"processing_time_ms": 4523,
"page_load_time_ms": 3456,
"page_load_rating": "average"
}
}Response Fields
Root Level Fields
- schema_version - API schema version (e.g., "1.0.0") for response structure
- run_id - Unique identifier for this performance measurement run
- url - The URL that was measured
- mode - Measurement mode: lab (controlled) or field (real users)
- environment - Environment and device configuration used for measurement
- summary - High-level performance summary with overall rating, warnings, errors, and main issues
Core Web Vitals
Each Core Web Vital includes the measured value (value_ms for time-based metrics, value for dimensionless metrics), rating (good/needs-improvement/poor), budget threshold, and pass/fail status.
- lcp - Largest Contentful Paint: measures when the largest content element becomes visible (budget: ≤2500ms)
- inp - Interaction to Next Paint: measures responsiveness to user interactions (budget: ≤200ms)
- cls - Cumulative Layout Shift: measures visual stability, lower is better (budget: ≤0.1)
- fcp - First Contentful Paint: measures when first text/image appears (budget: ≤1800ms)
- ttfb - Time to First Byte: measures server response time (budget: ≤800ms)
Performance Score
Overall performance score (0-100) calculated using weighted metrics based on Lighthouse 10+ methodology:
- LCP - 35% of score (most important for perceived load speed)
- INP - 35% of score (critical for interactivity)
- CLS - 15% of score (important for visual stability)
- FCP - 10% of score (secondary load metric)
- TTFB - 5% of score (server optimization)
Resources
Detailed breakdown of all resources loaded on the page:
- total_requests - Total number of HTTP requests
- transfer_kb - Total data transferred over network (compressed)
- decoded_kb - Total data after decompression
- first_party - Resources from same origin
- third_party - Resources from different origins
- by_type - Resources categorized by type (javascript, image, stylesheet, etc.) with optional metadata like main_thread_ms for JS and unoptimized count for images
- top_hosts - Top 5 hosts by data transfer
- caching - Cache analysis including cache headers, compression status, and estimated savings
Tips
Actionable optimization recommendations with severity levels (high/medium/low) and links to detailed documentation.
Cache
Results are cached for 24 hours. The cache object includes:
- cached - Whether this result was served from cache
- cache_key - Unique cache key based on URL, device, waitUntil, and timeout
- cached_at - When the result was cached (if applicable)
- ttl_seconds - Time-to-live in seconds (86400 = 24 hours)
- expires_at - When the cache expires
Metadata
- processing_time_ms - Total API processing time
- page_load_time_ms - Actual page load time (0 for cached results)
- page_load_rating - Load time rating: fast (<1500ms), average (1500-3000ms), or slow (>3000ms)
Error Responses
400 Bad Request - Validation Error
Returned when request parameters are invalid (missing URL, invalid device, invalid waitUntil, or timeout out of range).
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"http_status": 400,
"details": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": ["url"],
"message": "Required"
}
]
}
}400 Bad Request - Invalid URL
Returned when URL format is invalid or SSRF protection is triggered (localhost, private IPs).
408 Request Timeout
Returned when page navigation exceeds the timeout value.
502 Bad Gateway - Navigation Failed
Returned when the page fails to load (DNS resolution failure, connection refused, SSL certificate errors, invalid hostname).
503 Service Unavailable - Browser Crash
Returned when the browser crashes during measurement (excessive memory, infinite loops, WebGL/Canvas overload).
Usage Examples
Basic Performance Measurement
Measure performance with default settings (desktop, networkidle2, 30s timeout):
curl -X GET "https://api.example.com/performance?url=https://example.com" \
-H "X-API-Key: YOUR_API_KEY"Mobile Performance Test
Test mobile performance with faster timeout:
curl -X GET "https://api.example.com/performance?url=https://example.com&device=mobile&timeout=10000" \
-H "X-API-Key: YOUR_API_KEY"SPA with Network Idle
Measure Single Page Application with strict network idle condition:
curl -X GET "https://api.example.com/performance?url=https://app.example.com&waitUntil=networkidle0" \
-H "X-API-Key: YOUR_API_KEY"Extract Core Web Vitals
Use jq to extract just the Core Web Vitals scores:
curl -X GET "https://api.example.com/performance?url=https://example.com" \
-H "X-API-Key: YOUR_API_KEY" | \
jq '{
url: .url,
score: .performance_score.value,
lcp: .core_web_vitals.lcp.value_ms,
inp: .core_web_vitals.inp.value_ms,
cls: .core_web_vitals.cls.value
}'Use Cases
- Monitor website performance over time
- Compare performance across different pages
- Identify performance bottlenecks and optimization opportunities
- Track Core Web Vitals for SEO rankings
- Generate automated performance reports
- Competitive analysis and benchmarking
- CI/CD pipeline integration for performance regression testing
Code Examples
Here's how to use the performance endpoint in different languages:
JavaScript / Node.js
async function getPerformanceMetrics(url, options = {}) {
const params = new URLSearchParams({
url,
device: options.device || 'desktop',
waitUntil: options.waitUntil || 'networkidle2',
timeout: options.timeout || '30000'
});
const response = await fetch(
`https://api.example.com/performance?${params}`,
{
headers: {
'X-API-Key': process.env.API_KEY
}
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`API error: ${error.error.message}`);
}
return await response.json();
}
// Example usage
const metrics = await getPerformanceMetrics('https://example.com', {
device: 'mobile',
timeout: '10000'
});
console.log('Performance Score:', metrics.performance_score.value);
console.log('Overall:', metrics.summary.overall);
console.log('LCP:', metrics.core_web_vitals.lcp.value_ms, 'ms -', metrics.core_web_vitals.lcp.rating);
console.log('INP:', metrics.core_web_vitals.inp.value_ms, 'ms -', metrics.core_web_vitals.inp.rating);
console.log('CLS:', metrics.core_web_vitals.cls.value, '-', metrics.core_web_vitals.cls.rating);
// Check if cached
if (metrics.cache.cached) {
console.log('Result served from cache, cached at:', metrics.cache.cached_at);
}
// Display optimization tips
if (metrics.tips && metrics.tips.length > 0) {
console.log('\nOptimization Tips:');
metrics.tips.forEach(tip => {
console.log(`- [${tip.severity}] ${tip.message}`);
});
}Python
import requests
import os
def get_performance_metrics(url, device='desktop', wait_until='networkidle2', timeout=30000):
response = requests.get(
'https://api.example.com/performance',
params={
'url': url,
'device': device,
'waitUntil': wait_until,
'timeout': timeout
},
headers={
'X-API-Key': os.environ.get('API_KEY')
}
)
response.raise_for_status()
return response.json()
# Example usage
metrics = get_performance_metrics('https://example.com', device='mobile', timeout=10000)
print(f"Performance Score: {metrics['performance_score']['value']}")
print(f"Overall: {metrics['summary']['overall']}")
print(f"LCP: {metrics['core_web_vitals']['lcp']['value_ms']} ms - {metrics['core_web_vitals']['lcp']['rating']}")
print(f"INP: {metrics['core_web_vitals']['inp']['value_ms']} ms - {metrics['core_web_vitals']['inp']['rating']}")
print(f"CLS: {metrics['core_web_vitals']['cls']['value']} - {metrics['core_web_vitals']['cls']['rating']}")
# Check if cached
if metrics['cache']['cached']:
print(f"Result served from cache, cached at: {metrics['cache']['cached_at']}")
# Display optimization tips
if metrics.get('tips'):
print("\nOptimization Tips:")
for tip in metrics['tips']:
print(f"- [{tip['severity']}] {tip['message']}")Monitoring Script
Example script to monitor performance and detect issues:
async function monitorPerformance(urls) {
const results = [];
for (const url of urls) {
const metrics = await getPerformanceMetrics(url, { device: 'mobile' });
const issues = [];
// Check summary for overall health
if (metrics.summary.overall !== 'excellent' && metrics.summary.overall !== 'good') {
issues.push(`Overall rating is ${metrics.summary.overall}`);
}
// Check Core Web Vitals
if (metrics.core_web_vitals.lcp.status === 'fail' || metrics.core_web_vitals.lcp.status === 'warn') {
issues.push(`LCP: ${metrics.core_web_vitals.lcp.value_ms}ms (${metrics.core_web_vitals.lcp.rating})`);
}
if (metrics.core_web_vitals.inp.status === 'fail' || metrics.core_web_vitals.inp.status === 'warn') {
issues.push(`INP: ${metrics.core_web_vitals.inp.value_ms}ms (${metrics.core_web_vitals.inp.rating})`);
}
if (metrics.core_web_vitals.cls.status === 'fail' || metrics.core_web_vitals.cls.status === 'warn') {
issues.push(`CLS: ${metrics.core_web_vitals.cls.value} (${metrics.core_web_vitals.cls.rating})`);
}
// Check for high-severity optimization tips
const criticalTips = metrics.tips?.filter(tip => tip.severity === 'high') || [];
if (criticalTips.length > 0) {
issues.push(`${criticalTips.length} high-severity optimization tips`);
}
results.push({
url,
score: metrics.performance_score.value,
overall: metrics.summary.overall,
mainIssue: metrics.summary.main_issue,
issues,
hasIssues: issues.length > 0
});
}
return results;
}
// Example usage
const urls = [
'https://example.com',
'https://example.com/products',
'https://example.com/about'
];
const results = await monitorPerformance(urls);
// Report pages with issues
results
.filter(r => r.hasIssues)
.forEach(r => {
console.log(`⚠️ ${r.url} (Score: ${r.score}) has performance issues:`);
if (r.mainIssue) console.log(` Main issue: ${r.mainIssue}`);
r.issues.forEach(issue => console.log(` - ${issue}`));
});Best Practices
Device Testing
- Always test mobile performance - Most traffic is mobile, and Google uses mobile-first indexing
- Test all device types - Use mobile (375x667), tablet (768x1024), and desktop (1920x1080) viewports
- Mobile typically scores lower due to smaller viewports, slower CPUs, and higher latency
Wait Conditions
- Static sites - Use "load" or "domcontentloaded" for faster measurements
- Traditional server-rendered pages - Use "load"
- Modern SPAs with some async content - Use "networkidle2" (default)
- SPAs with heavy async content - Use "networkidle0" for accuracy (slower)
Timeout Settings
- Fast static sites - 5000-10000ms
- Average web pages - 30000ms (default)
- Slow or complex pages - 45000-60000ms
- Longer timeouts increase API response time - balance accuracy vs. speed
Performance Optimization
- Prioritize by weight - Focus on LCP (35%) and INP (35%) first, then CLS (15%), FCP (10%), and TTFB (5%)
- Act on high-severity tips - Fix within 1 week, medium within 1 month, low when convenient
- Monitor over time - Track performance trends and catch regressions early
- Understand caching - Results cached for 24 hours. To bypass, change any parameter or add cache-busting query params
Performance Ratings
The API uses these thresholds to rate metrics:
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP | ≤ 2500ms | 2500ms - 4000ms | > 4000ms |
| INP | ≤ 200ms | 200ms - 500ms | > 500ms |
| CLS | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |