Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Analyze performance with WebPageTest

Page performance is analyzed with WebPageTest to identify loading bottlenecks and optimization opportunities.

Utilities
Quick take
Typical fix time 30 min
  • Run 3+ tests to get consistent results
  • Test from locations near your users
  • Use filmstrip view to identify visual progress
  • Check waterfall for blocking resources and slow third-parties
Why it matters: WebPageTest reveals real-world performance issues that synthetic tools miss—like TTFB from actual locations, render-blocking patterns, and third-party script impact.

Rule Details

WebPageTest provides comprehensive performance analysis with real browser testing, network throttling, and detailed waterfall charts to identify optimization opportunities.

Code Examples

Online Testing

# Public WebPageTest (free)
# Visit: https://www.webpagetest.org/
 
# Test configuration options:
# - Test Location: Choose closest to your users
# - Browser: Chrome, Firefox, Safari, Edge
# - Connection: 3G, 4G, Cable, etc.
# - Number of Test Runs: 3-9 runs recommended
# - Repeat View: Test with primed cache

Key Metrics to Monitor

// Core Web Vitals from WebPageTest
const coreWebVitals = {
  // Largest Contentful Paint - Loading performance
  LCP: {
    good: '< 2.5s',
    needsImprovement: '2.5s - 4.0s',
    poor: '> 4.0s'
  },
 
  // First Input Delay - Interactivity
  FID: {
    good: '< 100ms',
    needsImprovement: '100ms - 300ms',
    poor: '> 300ms'
  },
 
  // Cumulative Layout Shift - Visual stability
  CLS: {
    good: '< 0.1',
    needsImprovement: '0.1 - 0.25',
    poor: '> 0.25'
  }
}
 
// Additional WebPageTest Metrics
const additionalMetrics = {
  TTFB: 'Time to First Byte - Server response time',
  FCP: 'First Contentful Paint - First content rendered',
  TTI: 'Time to Interactive - Page fully interactive',
  TBT: 'Total Blocking Time - Main thread blocking',
  SpeedIndex: 'Visual completeness over time',
  VisualComplete: 'Visually complete loading time'
}

Why It Matters

WebPageTest reveals real-world performance issues that synthetic tools miss—like TTFB from actual locations, render-blocking patterns, and third-party script impact.

WebPageTest API Integration

API Testing Script

// webpagetest-api.js
const WebPageTest = require('webpagetest')
const fs = require('fs').promises
 
class WebPageTestAnalyzer {
  constructor(apiKey, options = {}) {
    this.wpt = new WebPageTest('www.webpagetest.org', apiKey)
    this.options = {
      location: 'Dulles:Chrome',
      connectivity: '3G',
      runs: 3,
      firstViewOnly: false,
      video: true,
      lighthouse: true,
      ...options
    }
  }
 
  async runTest(url, customOptions = {}) {
    const testOptions = { ...this.options, ...customOptions }
 
    return new Promise((resolve, reject) => {
      this.wpt.runTest(url, testOptions, (err, data) => {
        if (err) {
          reject(err)
        } else {
          resolve(data)
        }
      })
    })
  }
 
  async waitForResults(testId) {
    return new Promise((resolve, reject) => {
      const checkResults = () => {
        this.wpt.getTestResults(testId, (err, data) => {
          if (err) {
            reject(err)
          } else if (data.statusCode === 200) {
            resolve(data)
          } else {
            // Test still running, check again
            setTimeout(checkResults, 10000) // Check every 10 seconds
          }
        })
      }
 
      checkResults()
    })
  }
 
  async analyzeResults(results) {
    const firstView = results.data.runs[1].firstView
    const repeatView = results.data.runs[1].repeatView
 
    return {
      url: results.data.url,
      testId: results.data.id,
      summary: results.data.summary,
      metrics: {
        firstView: {
          TTFB: firstView.TTFB,
          FCP: firstView.firstContentfulPaint,
          LCP: firstView.largestContentfulPaint,
          CLS: firstView.chromeUserTiming?.CumulativeLayoutShift,
          TBT: firstView.totalBlockingTime,
          TTI: firstView.TTI,
          SpeedIndex: firstView.SpeedIndex,
          loadTime: firstView.loadTime
        },
        repeatView: repeatView ? {
          TTFB: repeatView.TTFB,
          FCP: repeatView.firstContentfulPaint,
          LCP: repeatView.largestContentfulPaint,
          loadTime: repeatView.loadTime
        } : null
      },
      lighthouse: results.data.lighthouse,
      opportunities: this.extractOpportunities(results),
      waterfall: results.data.runs[1].firstView.requests
    }
  }
 
  extractOpportunities(results) {
    const opportunities = []
    const firstView = results.data.runs[1].firstView
 
    // Check for common optimization opportunities
    if (firstView.TTFB > 500) {
      opportunities.push({
        type: 'Server Response',
        issue: 'Slow Time to First Byte',
        value: `${firstView.TTFB}ms`,
        recommendation: 'Optimize server response time, consider CDN'
      })
    }
 
    if (firstView.requests.length > 100) {
      opportunities.push({
        type: 'HTTP Requests',
        issue: 'Too many HTTP requests',
        value: `${firstView.requests.length} requests`,
        recommendation: 'Bundle resources, use HTTP/2 push, eliminate unnecessary requests'
      })
    }
 
    // Check for uncompressed resources
    const uncompressed = firstView.requests.filter(req =>
      !req.headers?.response?.['content-encoding'] &&
      req.bytesIn > 1024 &&
      (req.contentType?.includes('text/') || req.contentType?.includes('application/'))
    )
 
    if (uncompressed.length > 0) {
      opportunities.push({
        type: 'Compression',
        issue: 'Uncompressed resources',
        value: `${uncompressed.length} resources`,
        recommendation: 'Enable gzip/brotli compression'
      })
    }
 
    return opportunities
  }
 
  async generateReport(analysis, filename = 'webpagetest-report.json') {
    const report = {
      timestamp: new Date().toISOString(),
      ...analysis,
      recommendations: this.generateRecommendations(analysis)
    }
 
    await fs.writeFile(filename, JSON.stringify(report, null, 2))
    return report
  }
 
  generateRecommendations(analysis) {
    const recommendations = []
    const metrics = analysis.metrics.firstView
 
    // LCP recommendations
    if (metrics.LCP > 2500) {
      recommendations.push({
        metric: 'LCP',
        priority: 'high',
        issue: 'Large Contentful Paint is slow',
        actions: [
          'Optimize largest element (image/text)',
          'Preload critical resources',
          'Eliminate render-blocking resources',
          'Use efficient image formats (WebP, AVIF)'
        ]
      })
    }
 
    // FCP recommendations
    if (metrics.FCP > 1800) {
      recommendations.push({
        metric: 'FCP',
        priority: 'high',
        issue: 'First Contentful Paint is slow',
        actions: [
          'Minimize critical resource chain',
          'Remove render-blocking CSS/JS',
          'Inline critical CSS',
          'Use resource hints (preload, prefetch)'
        ]
      })
    }
 
    // CLS recommendations
    if (metrics.CLS > 0.1) {
      recommendations.push({
        metric: 'CLS',
        priority: 'medium',
        issue: 'Cumulative Layout Shift is high',
        actions: [
          'Set explicit dimensions for images/videos',
          'Reserve space for ads and embeds',
          'Use font-display: swap carefully',
          'Avoid injecting content above existing content'
        ]
      })
    }
 
    return recommendations
  }
}
 
// Usage example
async function performanceAudit(url) {
  const analyzer = new WebPageTestAnalyzer(process.env.WPT_API_KEY)
 
  try {
    console.log(`Starting WebPageTest for ${url}...`)
    const testData = await analyzer.runTest(url, {
      location: 'Dulles:Chrome',
      connectivity: '3G',
      runs: 3
    })
 
    console.log(`Test started. ID: ${testData.data.testId}`)
    console.log(`View results: ${testData.data.userUrl}`)
 
    const results = await analyzer.waitForResults(testData.data.testId)
    const analysis = await analyzer.analyzeResults(results)
    const report = await analyzer.generateReport(analysis)
 
    console.log('\n=== Performance Analysis Results ===')
    console.log(`TTFB: ${analysis.metrics.firstView.TTFB}ms`)
    console.log(`FCP: ${analysis.metrics.firstView.FCP}ms`)
    console.log(`LCP: ${analysis.metrics.firstView.LCP}ms`)
    console.log(`CLS: ${analysis.metrics.firstView.CLS}`)
    console.log(`Speed Index: ${analysis.metrics.firstView.SpeedIndex}`)
 
    if (analysis.opportunities.length > 0) {
      console.log('\n=== Optimization Opportunities ===')
      analysis.opportunities.forEach(opp => {
        console.log(`${opp.type}: ${opp.issue} (${opp.value})`)
        console.log(`  → ${opp.recommendation}`)
      })
    }
 
    return report
 
  } catch (error) {
    console.error('Performance audit failed:', error)
    throw error
  }
}
 
// Command line usage
if (require.main === module) {
  const url = process.argv[2]
  if (!url) {
    console.error('Usage: node webpagetest-api.js <url>')
    process.exit(1)
  }
 
  performanceAudit(url).catch(console.error)
}
 
module.exports = WebPageTestAnalyzer

Build Tool Integration

Webpack Performance Budget Plugin

// webpack-performance-budget-plugin.js
const WebPageTestAnalyzer = require('./webpagetest-analyzer')
 
class PerformanceBudgetPlugin {
  constructor(options = {}) {
    this.options = {
      url: options.url,
      apiKey: options.apiKey,
      budgets: {
        TTFB: 500,
        FCP: 1800,
        LCP: 2500,
        CLS: 0.1,
        SpeedIndex: 3000,
        ...options.budgets
      },
      failOnBudgetExceeded: options.failOnBudgetExceeded || false
    }
  }
 
  apply(compiler) {
    compiler.hooks.afterEmit.tapAsync('PerformanceBudgetPlugin', async (compilation, callback) => {
      if (!this.options.url || !this.options.apiKey) {
        console.warn('WebPageTest URL or API key not configured, skipping performance test')
        callback()
        return
      }
 
      try {
        console.log('Running WebPageTest performance analysis...')
 
        const analyzer = new WebPageTestAnalyzer(this.options.apiKey)
        const testData = await analyzer.runTest(this.options.url)
        const results = await analyzer.waitForResults(testData.data.testId)
        const analysis = await analyzer.analyzeResults(results)
 
        const budgetResults = this.checkBudgets(analysis.metrics.firstView)
        const failed = budgetResults.filter(result => !result.passed)
 
        console.log('\n=== Performance Budget Results ===')
        budgetResults.forEach(result => {
          const status = result.passed ? '✅' : '❌'
          console.log(`${status} ${result.metric}: ${result.actual} (budget: ${result.budget})`)
        })
 
        if (failed.length > 0 && this.options.failOnBudgetExceeded) {
          callback(new Error(`${failed.length} performance budgets exceeded`))
        } else {
          callback()
        }
 
      } catch (error) {
        console.error('Performance budget check failed:', error.message)
        callback(this.options.failOnBudgetExceeded ? error : null)
      }
    })
  }
 
  checkBudgets(metrics) {
    return Object.entries(this.options.budgets).map(([metric, budget]) => {
      const actual = metrics[metric]
      const passed = actual <= budget
 
      return {
        metric,
        budget,
        actual,
        passed
      }
    })
  }
}
 
// webpack.config.js
module.exports = {
  plugins: [
    new PerformanceBudgetPlugin({
      url: 'https://your-deployed-site.com',
      apiKey: process.env.WPT_API_KEY,
      budgets: {
        TTFB: 300,
        FCP: 1500,
        LCP: 2000,
        SpeedIndex: 2500
      },
      failOnBudgetExceeded: process.env.NODE_ENV === 'production'
    })
  ]
}

GitHub Actions Integration

# .github/workflows/performance-testing.yml
name: Performance Testing
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    # Run performance tests daily
    - cron: '0 6 * * *'
 
jobs:
  webpagetest:
    runs-on: ubuntu-latest
 
    steps:
    - uses: actions/checkout@v4
 
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
 
    - name: Install dependencies
      run: npm ci
 
    - name: Build project
      run: npm run build
 
    - name: Deploy to staging
      run: |
        # Deploy your built site to a staging URL
        echo "Deploying to staging..."
        # Your deployment commands here
 
    - name: Run WebPageTest
      env:
        WPT_API_KEY: ${{ secrets.WEBPAGETEST_API_KEY }}
      run: |
        npm install -g webpagetest
        node scripts/webpagetest-runner.js https://staging.your-site.com
 
    - name: Generate Performance Report
      uses: actions/github-script@v7
      if: github.event_name == 'pull_request'
      with:
        script: |
          const fs = require('fs')
 
          if (fs.existsSync('webpagetest-report.json')) {
            const report = JSON.parse(fs.readFileSync('webpagetest-report.json', 'utf8'))
 
            const metrics = report.metrics.firstView
            const comment = `## 🚀 WebPageTest Performance Results
 
            ### Core Web Vitals
            - **LCP**: ${metrics.LCP}ms ${metrics.LCP <= 2500 ? '✅' : '❌'}
            - **FCP**: ${metrics.FCP}ms ${metrics.FCP <= 1800 ? '✅' : '❌'}
            - **CLS**: ${metrics.CLS} ${metrics.CLS <= 0.1 ? '✅' : '❌'}
 
            ### Additional Metrics
            - **TTFB**: ${metrics.TTFB}ms
            - **Speed Index**: ${metrics.SpeedIndex}
            - **TTI**: ${metrics.TTI}ms
 
            ${report.opportunities.length > 0 ? `
            ### Optimization Opportunities
            ${report.opportunities.map(opp => `- **${opp.type}**: ${opp.issue}`).join('\n')}
            ` : ''}
 
            [View detailed results](${report.testUrl})`
 
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: comment
            })
          }
 
    - name: Upload Performance Artifacts
      uses: actions/upload-artifact@v3
      with:
        name: performance-report
        path: |
          webpagetest-report.json
          webpagetest-waterfall.png
        retention-days: 30
 
# Performance budget enforcement
  performance-budget:
    runs-on: ubuntu-latest
    needs: webpagetest
 
    steps:
    - name: Check Performance Budget
      env:
        LCP_BUDGET: 2500
        FCP_BUDGET: 1800
        CLS_BUDGET: 0.1
      run: |
        node scripts/check-performance-budget.js

Advanced Analysis Techniques

Waterfall Analysis

// waterfall-analyzer.js
class WaterfallAnalyzer {
  constructor(waterfallData) {
    this.requests = waterfallData
  }
 
  analyzeRequestChains() {
    const chains = []
    const requestMap = new Map()
 
    // Build request dependency map
    this.requests.forEach(request => {
      requestMap.set(request.url, request)
 
      // Find requests that block others
      if (request.contentType?.includes('text/html')) {
        chains.push({
          type: 'HTML Document',
          url: request.url,
          startTime: request.load_start,
          endTime: request.load_end,
          blocksRendering: true
        })
      }
 
      if (request.contentType?.includes('text/css') && !request.async) {
        chains.push({
          type: 'Render-blocking CSS',
          url: request.url,
          startTime: request.load_start,
          endTime: request.load_end,
          blocksRendering: true
        })
      }
    })
 
    return chains.sort((a, b) => a.startTime - b.startTime)
  }
 
  identifyBottlenecks() {
    const bottlenecks = []
 
    // Find slow requests
    const slowRequests = this.requests
      .filter(req => req.load_end - req.load_start > 1000)
      .sort((a, b) => (b.load_end - b.load_start) - (a.load_end - a.load_start))
 
    slowRequests.forEach(request => {
      bottlenecks.push({
        type: 'Slow Request',
        url: request.url,
        duration: request.load_end - request.load_start,
        contentType: request.contentType,
        size: request.bytesIn
      })
    })
 
    // Find requests without caching
    const uncached = this.requests.filter(req =>
      !req.headers?.response?.['cache-control'] &&
      !req.headers?.response?.['expires'] &&
      req.bytesIn > 1024
    )
 
    uncached.forEach(request => {
      bottlenecks.push({
        type: 'Missing Cache Headers',
        url: request.url,
        size: request.bytesIn,
        recommendation: 'Add appropriate cache headers'
      })
    })
 
    return bottlenecks
  }
 
  analyzeResourceTiming() {
    const analysis = {
      totalRequests: this.requests.length,
      totalBytes: this.requests.reduce((sum, req) => sum + req.bytesIn, 0),
      resourceTypes: {},
      timingBreakdown: {
        dns: 0,
        connect: 0,
        ssl: 0,
        wait: 0,
        download: 0
      }
    }
 
    // Group by content type
    this.requests.forEach(request => {
      const type = request.contentType?.split('/')[0] || 'other'
 
      if (!analysis.resourceTypes[type]) {
        analysis.resourceTypes[type] = {
          count: 0,
          bytes: 0,
          avgDuration: 0
        }
      }
 
      analysis.resourceTypes[type].count++
      analysis.resourceTypes[type].bytes += request.bytesIn
      analysis.resourceTypes[type].avgDuration += request.load_end - request.load_start
 
      // Timing breakdown
      if (request.dns_start && request.dns_end) {
        analysis.timingBreakdown.dns += request.dns_end - request.dns_start
      }
      if (request.connect_start && request.connect_end) {
        analysis.timingBreakdown.connect += request.connect_end - request.connect_start
      }
    })
 
    // Calculate averages
    Object.values(analysis.resourceTypes).forEach(type => {
      type.avgDuration = type.avgDuration / type.count
    })
 
    return analysis
  }
 
  generateOptimizationPlan() {
    const chains = this.analyzeRequestChains()
    const bottlenecks = this.identifyBottlenecks()
    const resourceAnalysis = this.analyzeResourceTiming()
 
    const plan = []
 
    // Critical path optimization
    const renderBlockingResources = chains.filter(chain => chain.blocksRendering)
    if (renderBlockingResources.length > 3) {
      plan.push({
        priority: 'high',
        category: 'Critical Path',
        issue: `${renderBlockingResources.length} render-blocking resources`,
        actions: [
          'Inline critical CSS',
          'Defer non-critical JavaScript',
          'Use resource hints (preload, prefetch)',
          'Minimize critical resource chain'
        ]
      })
    }
 
    // Large resource optimization
    const largeResources = bottlenecks
      .filter(b => b.type === 'Slow Request' && b.size > 100000)
 
    if (largeResources.length > 0) {
      plan.push({
        priority: 'high',
        category: 'Resource Size',
        issue: `${largeResources.length} large resources detected`,
        actions: [
          'Compress images (WebP, AVIF)',
          'Minify CSS/JS',
          'Enable gzip/brotli compression',
          'Use lazy loading for non-critical resources'
        ]
      })
    }
 
    return plan
  }
}
 
// Usage
const analyzer = new WaterfallAnalyzer(waterfallData)
const optimizationPlan = analyzer.generateOptimizationPlan()

Performance Regression Detection

// performance-regression-detector.js
const fs = require('fs').promises
 
class PerformanceRegressionDetector {
  constructor(baselineFile = 'performance-baseline.json') {
    this.baselineFile = baselineFile
    this.thresholds = {
      TTFB: 0.2, // 20% regression threshold
      FCP: 0.15,
      LCP: 0.15,
      CLS: 0.1,
      SpeedIndex: 0.2
    }
  }
 
  async loadBaseline() {
    try {
      const baselineData = await fs.readFile(this.baselineFile, 'utf8')
      return JSON.parse(baselineData)
    } catch (error) {
      console.warn('No baseline found, creating new baseline')
      return null
    }
  }
 
  async saveBaseline(metrics) {
    const baseline = {
      timestamp: new Date().toISOString(),
      metrics,
      version: process.env.BUILD_VERSION || 'unknown'
    }
 
    await fs.writeFile(this.baselineFile, JSON.stringify(baseline, null, 2))
  }
 
  compareMetrics(baseline, current) {
    if (!baseline) return { isRegression: false, comparisons: [] }
 
    const comparisons = []
    let hasRegression = false
 
    Object.entries(this.thresholds).forEach(([metric, threshold]) => {
      const baselineValue = baseline.metrics[metric]
      const currentValue = current[metric]
 
      if (baselineValue && currentValue) {
        const change = (currentValue - baselineValue) / baselineValue
        const isRegression = change > threshold
 
        if (isRegression) hasRegression = true
 
        comparisons.push({
          metric,
          baseline: baselineValue,
          current: currentValue,
          change: Math.round(change * 100),
          isRegression,
          threshold: Math.round(threshold * 100)
        })
      }
    })
 
    return {
      isRegression: hasRegression,
      comparisons
    }
  }
 
  generateRegressionReport(comparison) {
    const regressions = comparison.comparisons.filter(c => c.isRegression)
 
    if (regressions.length === 0) {
      return {
        status: 'passed',
        message: 'No performance regressions detected',
        details: comparison.comparisons
      }
    }
 
    return {
      status: 'failed',
      message: `${regressions.length} performance regressions detected`,
      regressions: regressions.map(r => ({
        metric: r.metric,
        change: `+${r.change}%`,
        baseline: r.baseline,
        current: r.current,
        threshold: `${r.threshold}%`
      })),
      details: comparison.comparisons
    }
  }
 
  async checkForRegressions(currentMetrics) {
    const baseline = await this.loadBaseline()
    const comparison = this.compareMetrics(baseline, currentMetrics)
    const report = this.generateRegressionReport(comparison)
 
    if (report.status === 'passed' && !baseline) {
      // First run, save as baseline
      await this.saveBaseline(currentMetrics)
      report.message = 'Baseline established for future comparisons'
    }
 
    return report
  }
}
 
// Integration with WebPageTest
async function performanceRegressionCheck(url) {
  const analyzer = new WebPageTestAnalyzer(process.env.WPT_API_KEY)
  const detector = new PerformanceRegressionDetector()
 
  // Run WebPageTest
  const testData = await analyzer.runTest(url)
  const results = await analyzer.waitForResults(testData.data.testId)
  const analysis = await analyzer.analyzeResults(results)
 
  // Check for regressions
  const regressionReport = await detector.checkForRegressions(analysis.metrics.firstView)
 
  if (regressionReport.status === 'failed') {
    console.error('❌ Performance regression detected!')
    regressionReport.regressions.forEach(regression => {
      console.error(`  ${regression.metric}: ${regression.change} (threshold: ${regression.threshold})`)
    })
 
    if (process.env.CI) {
      process.exit(1) // Fail CI build
    }
  } else {
    console.log('✅ No performance regressions detected')
  }
 
  return regressionReport
}

Continuous Performance Monitoring

Performance Dashboard

// performance-dashboard.js
const express = require('express')
const { WebPageTestAnalyzer } = require('./webpagetest-analyzer')
 
class PerformanceDashboard {
  constructor(options = {}) {
    this.app = express()
    this.port = options.port || 3000
    this.sites = options.sites || []
    this.analyzer = new WebPageTestAnalyzer(options.apiKey)
    this.results = new Map()
 
    this.setupRoutes()
    this.scheduleTests()
  }
 
  setupRoutes() {
    this.app.use(express.static('public'))
    this.app.use(express.json())
 
    this.app.get('/api/results', (req, res) => {
      const allResults = Array.from(this.results.entries()).map(([site, data]) => ({
        site,
        ...data
      }))
      res.json(allResults)
    })
 
    this.app.get('/api/results/:site', (req, res) => {
      const result = this.results.get(req.params.site)
      if (result) {
        res.json(result)
      } else {
        res.status(404).json({ error: 'Site not found' })
      }
    })
 
    this.app.post('/api/test/:site', async (req, res) => {
      try {
        const site = this.sites.find(s => s.name === req.params.site)
        if (!site) {
          return res.status(404).json({ error: 'Site not found' })
        }
 
        const result = await this.runSingleTest(site)
        res.json(result)
      } catch (error) {
        res.status(500).json({ error: error.message })
      }
    })
  }
 
  async runSingleTest(site) {
    try {
      const testData = await this.analyzer.runTest(site.url, {
        location: site.location || 'Dulles:Chrome',
        connectivity: site.connectivity || '3G'
      })
 
      const results = await this.analyzer.waitForResults(testData.data.testId)
      const analysis = await this.analyzer.analyzeResults(results)
 
      const result = {
        ...analysis,
        timestamp: new Date().toISOString(),
        status: 'completed'
      }
 
      this.results.set(site.name, result)
      return result
 
    } catch (error) {
      const errorResult = {
        error: error.message,
        timestamp: new Date().toISOString(),
        status: 'failed'
      }
 
      this.results.set(site.name, errorResult)
      throw error
    }
  }
 
  scheduleTests() {
    const cron = require('node-cron')
 
    // Run tests every 6 hours
    cron.schedule('0 */6 * * *', async () => {
      console.log('Running scheduled performance tests...')
 
      for (const site of this.sites) {
        try {
          await this.runSingleTest(site)
          console.log(`✅ Test completed for ${site.name}`)
        } catch (error) {
          console.error(`❌ Test failed for ${site.name}:`, error.message)
        }
 
        // Add delay between tests to avoid overwhelming WebPageTest
        await new Promise(resolve => setTimeout(resolve, 30000))
      }
    })
  }
 
  start() {
    this.app.listen(this.port, () => {
      console.log(`Performance dashboard running on http://localhost:${this.port}`)
    })
  }
}
 
// Configuration
const dashboard = new PerformanceDashboard({
  apiKey: process.env.WPT_API_KEY,
  port: 3000,
  sites: [
    {
      name: 'production',
      url: 'https://example.com',
      location: 'Dulles:Chrome',
      connectivity: '3G'
    },
    {
      name: 'staging',
      url: 'https://staging.example.com',
      location: 'London:Chrome',
      connectivity: '4G'
    }
  ]
})
 
dashboard.start()

Best Practices

  1. Regular Testing: Schedule automated tests weekly or after major deployments
  2. Multiple Locations: Test from different geographic locations relevant to your users
  3. Device Testing: Include mobile and desktop testing scenarios
  4. Network Conditions: Test various connection speeds (3G, 4G, WiFi)
  5. Multiple Runs: Use 3-9 test runs for consistent results
  6. Performance Budgets: Set and enforce performance budgets
  7. Regression Detection: Compare results over time to catch regressions
  8. Actionable Reports: Focus on metrics that drive user experience improvements

Common WebPageTest Optimization Insights

Critical Path Analysis

// Identify critical rendering path issues
function analyzeCriticalPath(waterfallData) {
  const criticalResources = waterfallData.requests.filter(req =>
    req.contentType?.includes('text/html') ||
    (req.contentType?.includes('text/css') && !req.async) ||
    (req.contentType?.includes('javascript') && !req.async && !req.defer)
  )
 
  const criticalPathDuration = Math.max(...criticalResources.map(req => req.load_end))
 
  return {
    resources: criticalResources.length,
    duration: criticalPathDuration,
    recommendations: criticalResources.length > 4 ? [
      'Inline critical CSS',
      'Defer non-critical JavaScript',
      'Use resource hints',
      'Minimize render-blocking resources'
    ] : []
  }
}

Image Optimization Insights

// Analyze image optimization opportunities
function analyzeImageOptimization(waterfallData) {
  const images = waterfallData.requests.filter(req =>
    req.contentType?.startsWith('image/')
  )
 
  const opportunities = []
  let totalSavings = 0
 
  images.forEach(img => {
    // Check for unoptimized formats
    if (img.contentType === 'image/jpeg' || img.contentType === 'image/png') {
      if (img.bytesIn > 50000) { // Images larger than 50KB
        const estimatedSavings = img.bytesIn * 0.3 // Estimate 30% savings with WebP
        totalSavings += estimatedSavings
 
        opportunities.push({
          url: img.url,
          currentSize: img.bytesIn,
          estimatedSavings,
          recommendation: 'Convert to WebP/AVIF format'
        })
      }
    }
 
    // Check for missing dimensions (causes CLS)
    if (!img.headers?.response?.['content-length']) {
      opportunities.push({
        url: img.url,
        recommendation: 'Add explicit width/height attributes'
      })
    }
  })
 
  return {
    totalImages: images.length,
    totalSize: images.reduce((sum, img) => sum + img.bytesIn, 0),
    opportunities,
    estimatedSavings: totalSavings
  }
}

Support Notes

  • Performance-testing integrations can vary across browsers and agent types, so document which environment is the source of truth for this rule.
  • Use a fallback note when a tool-specific feature is not available in every supported testing environment.

Verification

Automated Checks

  • Inspect the final rendered HTML in the browser or page source to confirm the rule is satisfied.
  • Validate the affected markup with browser tooling or an HTML validator where appropriate.
  • Test one representative route or template that uses the pattern.
  • Re-check shared components that emit the same markup so the fix is consistent.

Manual Checks

  • Verify the rendered browser behavior manually on representative routes and supported browsers so the user-facing outcome matches the rule.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Use WebPageTest to analyze page performance, identify bottlenecks, and measure real-world loading metrics across different devices and connections.

Fix

Auto-fix issues

Implement recommended optimizations from WebPageTest results including resource compression, caching, and critical path improvements.

Explain

Learn more

Explain how WebPageTest provides insights into Core Web Vitals, loading waterfalls, and performance opportunities that synthetic testing can't reveal.

Review

Code review

Review templates, server-rendered HTML, and shared components that output markup related to Analyze performance with WebPageTest. Flag exact elements, attributes, and routes where the rendered HTML violates the rule.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Nu Html Checker
validator.w3.orgTool

Rules that often go hand-in-hand with this one.

Create a custom 404 error page

A custom 404 error page is designed with helpful navigation options for lost users.

HTML
Use resource hints for faster loading

Implement preload, prefetch, and preconnect hints to optimize resource loading priority.

Performance
Perform browser-based performance audits

Conduct performance audits in a full browser environment to capture accurate runtime metrics and layout shifts.

Performance
Eliminate render-blocking resources

Checks for render-blocking CSS and JavaScript that prevent the initial page render

Performance

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385