Skip to main content
Glama
webpagetest.ts9 kB
/** * WebPageTest Performance Analyzer (via Playwright automation) * Web UI: https://www.webpagetest.org/ * Free tier: 300 tests/month * * Uses Playwright to automate the web UI since the API requires paid access. */ import { Page } from 'playwright'; import { getBrowserManager } from '../shared/browser-utils.js'; export interface WebPageTestOptions { /** Test location (default: Dulles, VA - Chrome) */ location?: string; /** Connection type (default: Cable) */ connection?: string; /** Number of test runs (default: 1) */ runs?: number; /** Timeout in milliseconds (default: 300000 = 5 minutes) */ timeout?: number; /** Wait for test to complete before returning (default: false) */ waitForResults?: boolean; } export interface WebPageTestMetrics { loadTime?: number; firstContentfulPaint?: number; speedIndex?: number; largestContentfulPaint?: number; timeToInteractive?: number; totalBlockingTime?: number; cumulativeLayoutShift?: number; } export interface WebPageTestResult { tool: 'webpagetest'; success: boolean; url: string; test_id?: string; results_url?: string; summary?: WebPageTestMetrics; performance_grade?: string; security_grade?: string; status: 'pending' | 'running' | 'complete' | 'error'; error?: string; } /** * Submit a test to WebPageTest via web UI automation */ async function submitWebPageTest( url: string, options: WebPageTestOptions = {} ): Promise<{ testId: string; resultsUrl: string }> { const browserManager = await getBrowserManager(); const page = await browserManager.newPage(); try { const timeout = options.timeout || 300000; // 5 minutes default // Navigate to WebPageTest await page.goto('https://www.webpagetest.org/', { timeout }); // Enter URL in the test input await page.fill('input[name="url"]', url); // Select location if provided (default is usually Dulles:Chrome) if (options.location) { await page.selectOption('select[name="location"]', options.location); } // Set number of runs if provided if (options.runs) { await page.fill('input[name="runs"]', options.runs.toString()); } // Submit the test await Promise.all([ page.waitForNavigation({ timeout }), page.click('input[type="submit"], button[type="submit"]'), ]); // Wait for redirect to results page await page.waitForURL(/.*\/result\/.*/, { timeout }); const resultsUrl = page.url(); const testIdMatch = resultsUrl.match(/\/result\/([^\/]+)/); if (!testIdMatch) { throw new Error('Could not extract test ID from results URL'); } const testId = testIdMatch[1]; return { testId, resultsUrl }; } finally { await page.close(); } } /** * Poll WebPageTest results page until test completes */ async function waitForWebPageTestResults( testId: string, timeout: number = 300000 ): Promise<WebPageTestMetrics> { const browserManager = await getBrowserManager(); const page = await browserManager.newPage(); try { const resultsUrl = `https://www.webpagetest.org/result/${testId}/`; await page.goto(resultsUrl, { timeout }); // Wait for test to complete (look for results table) await page.waitForSelector('.results-container, #test-results, .result', { timeout, state: 'visible', }); // Give the page a moment to fully render await page.waitForTimeout(2000); // Scrape metrics from the results page const metrics: WebPageTestMetrics = {}; // Try to extract key metrics // Note: WebPageTest's DOM structure may vary, so we try multiple selectors // Load Time const loadTimeText = await page.textContent('[data-metric="loadTime"], .loadTime, td:has-text("Load Time") + td').catch(() => null); if (loadTimeText) { metrics.loadTime = parseFloat(loadTimeText.replace(/[^0-9.]/g, '')); } // First Contentful Paint const fcpText = await page.textContent('[data-metric="firstContentfulPaint"], .firstContentfulPaint').catch(() => null); if (fcpText) { metrics.firstContentfulPaint = parseFloat(fcpText.replace(/[^0-9.]/g, '')); } // Speed Index const siText = await page.textContent('[data-metric="speedIndex"], .speedIndex').catch(() => null); if (siText) { metrics.speedIndex = parseFloat(siText.replace(/[^0-9.]/g, '')); } // Largest Contentful Paint const lcpText = await page.textContent('[data-metric="largestContentfulPaint"], .largestContentfulPaint').catch(() => null); if (lcpText) { metrics.largestContentfulPaint = parseFloat(lcpText.replace(/[^0-9.]/g, '')); } // Time to Interactive const ttiText = await page.textContent('[data-metric="timeToInteractive"], .timeToInteractive').catch(() => null); if (ttiText) { metrics.timeToInteractive = parseFloat(ttiText.replace(/[^0-9.]/g, '')); } // Total Blocking Time const tbtText = await page.textContent('[data-metric="totalBlockingTime"], .totalBlockingTime').catch(() => null); if (tbtText) { metrics.totalBlockingTime = parseFloat(tbtText.replace(/[^0-9.]/g, '')); } // Cumulative Layout Shift const clsText = await page.textContent('[data-metric="cumulativeLayoutShift"], .cumulativeLayoutShift').catch(() => null); if (clsText) { metrics.cumulativeLayoutShift = parseFloat(clsText.replace(/[^0-9.]/g, '')); } return metrics; } finally { await page.close(); } } /** * Extract grades from WebPageTest results page */ async function getWebPageTestGrades(testId: string): Promise<{ performance?: string; security?: string }> { const browserManager = await getBrowserManager(); const page = await browserManager.newPage(); try { const resultsUrl = `https://www.webpagetest.org/result/${testId}/`; await page.goto(resultsUrl, { timeout: 30000 }); const grades: { performance?: string; security?: string } = {}; // Try to find performance grade const perfGrade = await page.textContent('.performance-grade, [data-grade="performance"]').catch(() => null); if (perfGrade) { grades.performance = perfGrade.trim(); } // Try to find security grade const secGrade = await page.textContent('.security-grade, [data-grade="security"]').catch(() => null); if (secGrade) { grades.security = secGrade.trim(); } return grades; } finally { await page.close(); } } /** * Analyze website using WebPageTest * * @param url - The URL to test * @param options - Test configuration options * @returns WebPageTest results (immediate if waitForResults=false, complete if waitForResults=true) */ export async function analyzeWebPageTest( url: string, options: WebPageTestOptions = {} ): Promise<WebPageTestResult> { try { // Submit test const { testId, resultsUrl } = await submitWebPageTest(url, options); // If not waiting for results, return immediately with test ID if (!options.waitForResults) { return { tool: 'webpagetest', success: true, url, test_id: testId, results_url: resultsUrl, status: 'pending', }; } // Wait for results const metrics = await waitForWebPageTestResults(testId, options.timeout); const grades = await getWebPageTestGrades(testId); return { tool: 'webpagetest', success: true, url, test_id: testId, results_url: resultsUrl, summary: metrics, performance_grade: grades.performance, security_grade: grades.security, status: 'complete', }; } catch (error) { return { tool: 'webpagetest', success: false, url, status: 'error', error: error instanceof Error ? error.message : String(error), }; } } /** * Submit a WebPageTest and return test ID immediately * Use this for async testing, then poll with getWebPageTestResults() */ export async function submitWebPageTestAsync( url: string, options: WebPageTestOptions = {} ): Promise<WebPageTestResult> { return analyzeWebPageTest(url, { ...options, waitForResults: false }); } /** * Get results for a previously submitted WebPageTest */ export async function getWebPageTestResults(testId: string): Promise<WebPageTestResult> { try { const metrics = await waitForWebPageTestResults(testId); const grades = await getWebPageTestGrades(testId); return { tool: 'webpagetest', success: true, url: `https://www.webpagetest.org/result/${testId}/`, test_id: testId, results_url: `https://www.webpagetest.org/result/${testId}/`, summary: metrics, performance_grade: grades.performance, security_grade: grades.security, status: 'complete', }; } catch (error) { return { tool: 'webpagetest', success: false, url: '', test_id: testId, status: 'error', error: error instanceof Error ? error.message : String(error), }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cordlesssteve/webby-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server