Files
2025-11-30 08:48:52 +08:00

21 KiB

Performance Analysis Guide

Comprehensive guide to analyzing web performance using Chrome DevTools Protocol, Puppeteer, and chrome-devtools skill.

Table of Contents


Core Web Vitals

Overview

Core Web Vitals are Google's standardized metrics for measuring user experience:

  • LCP (Largest Contentful Paint) - Loading performance (< 2.5s good)
  • FID (First Input Delay) - Interactivity (< 100ms good)
  • CLS (Cumulative Layout Shift) - Visual stability (< 0.1 good)

Measuring with chrome-devtools-mcp

// Start performance trace
await useTool('performance_start_trace', {
  categories: ['loading', 'rendering', 'scripting']
});

// Navigate to page
await useTool('navigate_page', {
  url: 'https://example.com'
});

// Wait for complete load
await useTool('wait_for', {
  waitUntil: 'networkidle'
});

// Stop trace and get data
await useTool('performance_stop_trace');

// Get AI-powered insights
const insights = await useTool('performance_analyze_insight');

// insights will include:
// - LCP timing
// - FID analysis
// - CLS score
// - Performance recommendations

Measuring with Puppeteer

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// Measure Core Web Vitals
await page.goto('https://example.com', {
  waitUntil: 'networkidle2'
});

const vitals = await page.evaluate(() => {
  return new Promise((resolve) => {
    const vitals = {
      LCP: null,
      FID: null,
      CLS: 0
    };

    // LCP
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      vitals.LCP = entries[entries.length - 1].renderTime ||
                   entries[entries.length - 1].loadTime;
    }).observe({ entryTypes: ['largest-contentful-paint'] });

    // FID
    new PerformanceObserver((list) => {
      vitals.FID = list.getEntries()[0].processingStart -
                   list.getEntries()[0].startTime;
    }).observe({ entryTypes: ['first-input'] });

    // CLS
    new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (!entry.hadRecentInput) {
          vitals.CLS += entry.value;
        }
      });
    }).observe({ entryTypes: ['layout-shift'] });

    // Wait 5 seconds for metrics
    setTimeout(() => resolve(vitals), 5000);
  });
});

console.log('Core Web Vitals:', vitals);

Other Important Metrics

TTFB (Time to First Byte)

const ttfb = await page.evaluate(() => {
  const [navigationEntry] = performance.getEntriesByType('navigation');
  return navigationEntry.responseStart - navigationEntry.requestStart;
});

FCP (First Contentful Paint)

const fcp = await page.evaluate(() => {
  const paintEntries = performance.getEntriesByType('paint');
  const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
  return fcpEntry ? fcpEntry.startTime : null;
});

TTI (Time to Interactive)

// Requires lighthouse or manual calculation
const tti = await page.evaluate(() => {
  // Complex calculation based on network idle and long tasks
  // Best to use Lighthouse for accurate TTI
});

Performance Tracing

Chrome Trace Categories

Loading:

  • Page load events
  • Resource loading
  • Parser activity

Rendering:

  • Layout calculations
  • Paint operations
  • Compositing

Scripting:

  • JavaScript execution
  • V8 compilation
  • Garbage collection

Network:

  • HTTP requests
  • WebSocket traffic
  • Resource fetching

Input:

  • User input processing
  • Touch/scroll events

GPU:

  • GPU operations
  • Compositing work

Record Performance Trace

Using chrome-devtools-mcp:

// Start trace with specific categories
await useTool('performance_start_trace', {
  categories: ['loading', 'rendering', 'scripting', 'network']
});

// Perform actions
await useTool('navigate_page', { url: 'https://example.com' });
await useTool('wait_for', { waitUntil: 'networkidle' });

// Optional: Interact with page
await useTool('click', { uid: 'button-uid' });

// Stop trace
const traceData = await useTool('performance_stop_trace');

// Analyze trace
const insights = await useTool('performance_analyze_insight');

Using Puppeteer:

// Start tracing
await page.tracing.start({
  path: 'trace.json',
  categories: [
    'devtools.timeline',
    'disabled-by-default-devtools.timeline',
    'disabled-by-default-v8.cpu_profiler'
  ]
});

// Navigate
await page.goto('https://example.com', {
  waitUntil: 'networkidle2'
});

// Stop tracing
await page.tracing.stop();

// Analyze in Chrome DevTools (chrome://tracing)

Analyze Trace Data

Key Metrics from Trace:

  1. Main Thread Activity

    • JavaScript execution time
    • Layout/reflow time
    • Paint time
    • Long tasks (> 50ms)
  2. Network Waterfall

    • Request start times
    • DNS lookup
    • Connection time
    • Download time
  3. Rendering Pipeline

    • DOM construction
    • Style calculation
    • Layout
    • Paint
    • Composite

Common Issues to Look For:

  • Long tasks blocking main thread
  • Excessive JavaScript execution
  • Layout thrashing
  • Unnecessary repaints
  • Slow network requests
  • Large bundle sizes

Network Analysis

Monitor Network Requests

Using chrome-devtools-mcp:

// Navigate to page
await useTool('navigate_page', { url: 'https://example.com' });

// Wait for all requests
await useTool('wait_for', { waitUntil: 'networkidle' });

// List all requests
const requests = await useTool('list_network_requests', {
  resourceTypes: ['Document', 'Script', 'Stylesheet', 'Image', 'XHR', 'Fetch'],
  pageSize: 100
});

// Analyze specific request
for (const req of requests.requests) {
  const details = await useTool('get_network_request', {
    requestId: req.id
  });

  console.log({
    url: details.url,
    method: details.method,
    status: details.status,
    size: details.encodedDataLength,
    time: details.timing.receiveHeadersEnd - details.timing.requestTime,
    cached: details.fromCache
  });
}

Using Puppeteer:

const requests = [];

// Capture all requests
page.on('request', (request) => {
  requests.push({
    url: request.url(),
    method: request.method(),
    resourceType: request.resourceType(),
    headers: request.headers()
  });
});

// Capture responses
page.on('response', (response) => {
  const request = response.request();
  console.log({
    url: response.url(),
    status: response.status(),
    size: response.headers()['content-length'],
    cached: response.fromCache(),
    timing: response.timing()
  });
});

await page.goto('https://example.com');

Network Performance Metrics

Calculate Total Page Weight:

let totalBytes = 0;
let resourceCounts = {};

page.on('response', async (response) => {
  const type = response.request().resourceType();
  const buffer = await response.buffer();

  totalBytes += buffer.length;
  resourceCounts[type] = (resourceCounts[type] || 0) + 1;
});

await page.goto('https://example.com');

console.log('Total size:', (totalBytes / 1024 / 1024).toFixed(2), 'MB');
console.log('Resources:', resourceCounts);

Identify Slow Requests:

page.on('response', (response) => {
  const timing = response.timing();
  const totalTime = timing.receiveHeadersEnd - timing.requestTime;

  if (totalTime > 1000) { // Slower than 1 second
    console.log('Slow request:', {
      url: response.url(),
      time: totalTime.toFixed(2) + 'ms',
      size: response.headers()['content-length']
    });
  }
});

Network Throttling

Simulate Slow Connection:

// Using chrome-devtools-mcp
await useTool('emulate_network', {
  throttlingOption: 'Slow 3G'  // or 'Fast 3G', 'Slow 4G'
});

// Using Puppeteer
const client = await page.createCDPSession();
await client.send('Network.emulateNetworkConditions', {
  offline: false,
  downloadThroughput: 400 * 1024 / 8,  // 400 Kbps
  uploadThroughput: 400 * 1024 / 8,
  latency: 2000  // 2000ms RTT
});

JavaScript Performance

Identify Long Tasks

Using Performance Observer:

await page.evaluate(() => {
  return new Promise((resolve) => {
    const longTasks = [];

    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        longTasks.push({
          name: entry.name,
          duration: entry.duration,
          startTime: entry.startTime
        });
      });
    });

    observer.observe({ entryTypes: ['longtask'] });

    // Collect for 10 seconds
    setTimeout(() => {
      observer.disconnect();
      resolve(longTasks);
    }, 10000);
  });
});

CPU Profiling

Using Puppeteer:

// Start CPU profiling
const client = await page.createCDPSession();
await client.send('Profiler.enable');
await client.send('Profiler.start');

// Navigate and interact
await page.goto('https://example.com');
await page.click('.button');

// Stop profiling
const { profile } = await client.send('Profiler.stop');

// Analyze profile (flame graph data)
// Import into Chrome DevTools for visualization

JavaScript Coverage

Identify Unused Code:

// Start coverage
await Promise.all([
  page.coverage.startJSCoverage(),
  page.coverage.startCSSCoverage()
]);

// Navigate
await page.goto('https://example.com');

// Stop coverage
const [jsCoverage, cssCoverage] = await Promise.all([
  page.coverage.stopJSCoverage(),
  page.coverage.stopCSSCoverage()
]);

// Calculate unused bytes
function calculateUnusedBytes(coverage) {
  let usedBytes = 0;
  let totalBytes = 0;

  for (const entry of coverage) {
    totalBytes += entry.text.length;
    for (const range of entry.ranges) {
      usedBytes += range.end - range.start - 1;
    }
  }

  return {
    usedBytes,
    totalBytes,
    unusedBytes: totalBytes - usedBytes,
    unusedPercentage: ((totalBytes - usedBytes) / totalBytes * 100).toFixed(2)
  };
}

console.log('JS Coverage:', calculateUnusedBytes(jsCoverage));
console.log('CSS Coverage:', calculateUnusedBytes(cssCoverage));

Bundle Size Analysis

Analyze JavaScript Bundles:

page.on('response', async (response) => {
  const url = response.url();
  const type = response.request().resourceType();

  if (type === 'script') {
    const buffer = await response.buffer();
    const size = buffer.length;

    console.log({
      url: url.split('/').pop(),
      size: (size / 1024).toFixed(2) + ' KB',
      gzipped: response.headers()['content-encoding'] === 'gzip'
    });
  }
});

Rendering Performance

Layout Thrashing Detection

Monitor Layout Recalculations:

// Using Performance Observer
await page.evaluate(() => {
  return new Promise((resolve) => {
    const measurements = [];

    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.entryType === 'measure' &&
            entry.name.includes('layout')) {
          measurements.push({
            name: entry.name,
            duration: entry.duration,
            startTime: entry.startTime
          });
        }
      });
    });

    observer.observe({ entryTypes: ['measure'] });

    setTimeout(() => {
      observer.disconnect();
      resolve(measurements);
    }, 5000);
  });
});

Paint and Composite Metrics

Get Paint Metrics:

const paintMetrics = await page.evaluate(() => {
  const paints = performance.getEntriesByType('paint');
  return {
    firstPaint: paints.find(p => p.name === 'first-paint')?.startTime,
    firstContentfulPaint: paints.find(p => p.name === 'first-contentful-paint')?.startTime
  };
});

Frame Rate Analysis

Monitor FPS:

await page.evaluate(() => {
  return new Promise((resolve) => {
    let frames = 0;
    let lastTime = performance.now();

    function countFrames() {
      frames++;
      requestAnimationFrame(countFrames);
    }

    countFrames();

    setTimeout(() => {
      const now = performance.now();
      const elapsed = (now - lastTime) / 1000;
      const fps = frames / elapsed;
      resolve(fps);
    }, 5000);
  });
});

Layout Shifts (CLS)

Track Individual Shifts:

await page.evaluate(() => {
  return new Promise((resolve) => {
    const shifts = [];
    let totalCLS = 0;

    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (!entry.hadRecentInput) {
          totalCLS += entry.value;
          shifts.push({
            value: entry.value,
            time: entry.startTime,
            elements: entry.sources?.map(s => s.node)
          });
        }
      });
    });

    observer.observe({ entryTypes: ['layout-shift'] });

    setTimeout(() => {
      observer.disconnect();
      resolve({ totalCLS, shifts });
    }, 10000);
  });
});

Memory Analysis

Memory Metrics

Get Memory Usage:

// Using chrome-devtools-mcp
await useTool('evaluate_script', {
  expression: `
    ({
      usedJSHeapSize: performance.memory?.usedJSHeapSize,
      totalJSHeapSize: performance.memory?.totalJSHeapSize,
      jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit
    })
  `,
  returnByValue: true
});

// Using Puppeteer
const metrics = await page.metrics();
console.log({
  jsHeapUsed: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2) + ' MB',
  jsHeapTotal: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2) + ' MB',
  domNodes: metrics.Nodes,
  documents: metrics.Documents,
  jsEventListeners: metrics.JSEventListeners
});

Memory Leak Detection

Monitor Memory Over Time:

async function detectMemoryLeak(page, duration = 30000) {
  const samples = [];
  const interval = 1000; // Sample every second
  const samples_count = duration / interval;

  for (let i = 0; i < samples_count; i++) {
    const metrics = await page.metrics();
    samples.push({
      time: i,
      heapUsed: metrics.JSHeapUsedSize
    });

    await page.waitForTimeout(interval);
  }

  // Analyze trend
  const firstSample = samples[0].heapUsed;
  const lastSample = samples[samples.length - 1].heapUsed;
  const increase = ((lastSample - firstSample) / firstSample * 100).toFixed(2);

  return {
    samples,
    memoryIncrease: increase + '%',
    possibleLeak: increase > 50 // > 50% increase indicates possible leak
  };
}

const leakAnalysis = await detectMemoryLeak(page, 30000);
console.log('Memory Analysis:', leakAnalysis);

Heap Snapshot

Capture Heap Snapshot:

const client = await page.createCDPSession();

// Take snapshot
await client.send('HeapProfiler.enable');
const { result } = await client.send('HeapProfiler.takeHeapSnapshot');

// Snapshot is streamed in chunks
// Save to file or analyze programmatically

Optimization Strategies

Image Optimization

Detect Unoptimized Images:

const images = await page.evaluate(() => {
  const images = Array.from(document.querySelectorAll('img'));
  return images.map(img => ({
    src: img.src,
    naturalWidth: img.naturalWidth,
    naturalHeight: img.naturalHeight,
    displayWidth: img.width,
    displayHeight: img.height,
    oversized: img.naturalWidth > img.width * 1.5 ||
               img.naturalHeight > img.height * 1.5
  }));
});

const oversizedImages = images.filter(img => img.oversized);
console.log('Oversized images:', oversizedImages);

Font Loading

Detect Render-Blocking Fonts:

const fonts = await page.evaluate(() => {
  return Array.from(document.fonts).map(font => ({
    family: font.family,
    weight: font.weight,
    style: font.style,
    status: font.status,
    loaded: font.status === 'loaded'
  }));
});

console.log('Fonts:', fonts);

Third-Party Scripts

Measure Third-Party Impact:

const thirdPartyDomains = ['googletagmanager.com', 'facebook.net', 'doubleclick.net'];

page.on('response', async (response) => {
  const url = response.url();
  const isThirdParty = thirdPartyDomains.some(domain => url.includes(domain));

  if (isThirdParty) {
    const buffer = await response.buffer();
    console.log({
      url: url,
      size: (buffer.length / 1024).toFixed(2) + ' KB',
      type: response.request().resourceType()
    });
  }
});

Critical Rendering Path

Identify Render-Blocking Resources:

await page.goto('https://example.com');

const renderBlockingResources = await page.evaluate(() => {
  const resources = performance.getEntriesByType('resource');
  return resources.filter(resource => {
    return (resource.initiatorType === 'link' &&
            resource.name.includes('.css')) ||
           (resource.initiatorType === 'script' &&
            !resource.name.includes('async'));
  }).map(r => ({
    url: r.name,
    duration: r.duration,
    startTime: r.startTime
  }));
});

console.log('Render-blocking resources:', renderBlockingResources);

Lighthouse Integration

Run Lighthouse Audit:

import lighthouse from 'lighthouse';
import { launch } from 'chrome-launcher';

// Launch Chrome
const chrome = await launch({ chromeFlags: ['--headless'] });

// Run Lighthouse
const { lhr } = await lighthouse('https://example.com', {
  port: chrome.port,
  onlyCategories: ['performance']
});

// Get scores
console.log({
  performanceScore: lhr.categories.performance.score * 100,
  metrics: {
    FCP: lhr.audits['first-contentful-paint'].displayValue,
    LCP: lhr.audits['largest-contentful-paint'].displayValue,
    TBT: lhr.audits['total-blocking-time'].displayValue,
    CLS: lhr.audits['cumulative-layout-shift'].displayValue,
    SI: lhr.audits['speed-index'].displayValue
  },
  opportunities: lhr.audits['opportunities']
});

await chrome.kill();

Performance Budgets

Set Performance Budgets

const budgets = {
  // Core Web Vitals
  LCP: 2500,        // ms
  FID: 100,         // ms
  CLS: 0.1,         // score

  // Other metrics
  FCP: 1800,        // ms
  TTI: 3800,        // ms
  TBT: 300,         // ms

  // Resource budgets
  totalPageSize: 2 * 1024 * 1024,  // 2 MB
  jsSize: 500 * 1024,               // 500 KB
  cssSize: 100 * 1024,              // 100 KB
  imageSize: 1 * 1024 * 1024,       // 1 MB

  // Request counts
  totalRequests: 50,
  jsRequests: 10,
  cssRequests: 5
};

async function checkBudgets(page, budgets) {
  // Measure actual values
  const vitals = await measureCoreWebVitals(page);
  const resources = await analyzeResources(page);

  // Compare against budgets
  const violations = [];

  if (vitals.LCP > budgets.LCP) {
    violations.push(`LCP: ${vitals.LCP}ms exceeds budget of ${budgets.LCP}ms`);
  }

  if (resources.totalSize > budgets.totalPageSize) {
    violations.push(`Page size: ${resources.totalSize} exceeds budget of ${budgets.totalPageSize}`);
  }

  // ... check other budgets

  return {
    passed: violations.length === 0,
    violations
  };
}

Automated Performance Testing

CI/CD Integration

// performance-test.js
import puppeteer from 'puppeteer';

async function performanceTest(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // Measure metrics
  await page.goto(url, { waitUntil: 'networkidle2' });
  const metrics = await page.metrics();
  const vitals = await measureCoreWebVitals(page);

  await browser.close();

  // Check against thresholds
  const thresholds = {
    LCP: 2500,
    FID: 100,
    CLS: 0.1,
    jsHeapSize: 50 * 1024 * 1024  // 50 MB
  };

  const failed = [];
  if (vitals.LCP > thresholds.LCP) failed.push('LCP');
  if (vitals.FID > thresholds.FID) failed.push('FID');
  if (vitals.CLS > thresholds.CLS) failed.push('CLS');
  if (metrics.JSHeapUsedSize > thresholds.jsHeapSize) failed.push('Memory');

  if (failed.length > 0) {
    console.error('Performance test failed:', failed);
    process.exit(1);
  }

  console.log('Performance test passed');
}

performanceTest(process.env.TEST_URL);

Best Practices

Performance Testing Checklist

  1. Measure Multiple Times

    • Run tests 3-5 times
    • Use median values
    • Account for variance
  2. Test Different Conditions

    • Fast 3G
    • Slow 3G
    • Offline
    • CPU throttling
  3. Test Different Devices

    • Mobile (low-end)
    • Mobile (high-end)
    • Desktop
    • Tablet
  4. Monitor Over Time

    • Track metrics in CI/CD
    • Set up alerts for regressions
    • Create performance dashboards
  5. Focus on User Experience

    • Prioritize Core Web Vitals
    • Test real user journeys
    • Consider perceived performance
  6. Optimize Critical Path

    • Minimize render-blocking resources
    • Defer non-critical JavaScript
    • Optimize font loading
    • Lazy load images

Resources