javascript

examples

examples.js
/**
 * 19.5 Performance Observer - Examples
 *
 * Monitor and analyze performance metrics
 */

// ============================================
// BASIC PERFORMANCE OBSERVER
// ============================================

/**
 * Simple performance monitoring
 */
function basicPerformanceObserver() {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log('=== Performance Entry ===');
      console.log('Name:', entry.name);
      console.log('Type:', entry.entryType);
      console.log('Duration:', entry.duration.toFixed(2), 'ms');
      console.log('Start Time:', entry.startTime.toFixed(2), 'ms');
    }
  });

  // Observe multiple entry types
  try {
    observer.observe({ entryTypes: ['mark', 'measure', 'resource'] });
  } catch (e) {
    console.warn('Some entry types not supported');
  }

  return observer;
}

// ============================================
// CUSTOM MARKS AND MEASURES
// ============================================

/**
 * Performance timing utility
 */
class PerformanceTimer {
  constructor() {
    this.marks = new Map();
    this.measures = [];
  }

  start(name) {
    const markName = `${name}-start`;
    performance.mark(markName);
    this.marks.set(name, markName);
    return this;
  }

  end(name, options = {}) {
    const startMark = this.marks.get(name);
    if (!startMark) {
      console.warn(`No start mark found for: ${name}`);
      return null;
    }

    const endMark = `${name}-end`;
    performance.mark(endMark);

    const measure = performance.measure(name, startMark, endMark);

    this.measures.push({
      name,
      duration: measure.duration,
      startTime: measure.startTime,
    });

    if (!options.keepMarks) {
      performance.clearMarks(startMark);
      performance.clearMarks(endMark);
    }

    return measure.duration;
  }

  measure(name, callback) {
    this.start(name);
    const result = callback();

    if (result instanceof Promise) {
      return result.finally(() => this.end(name));
    }

    this.end(name);
    return result;
  }

  async measureAsync(name, asyncCallback) {
    this.start(name);
    try {
      return await asyncCallback();
    } finally {
      this.end(name);
    }
  }

  getReport() {
    return this.measures.map((m) => ({
      ...m,
      durationFormatted: `${m.duration.toFixed(2)}ms`,
    }));
  }

  clear() {
    this.marks.clear();
    this.measures = [];
    performance.clearMarks();
    performance.clearMeasures();
  }
}

// ============================================
// CORE WEB VITALS MONITORING
// ============================================

/**
 * Monitor Core Web Vitals
 */
function monitorWebVitals(callback) {
  const vitals = {
    LCP: null,
    FID: null,
    CLS: null,
    FCP: null,
    TTFB: null,
  };

  // Largest Contentful Paint
  const lcpObserver = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    vitals.LCP = lastEntry.startTime;
    callback('LCP', vitals.LCP, getRating('LCP', vitals.LCP));
  });

  try {
    lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
  } catch (e) {
    console.warn('LCP not supported');
  }

  // First Input Delay
  const fidObserver = new PerformanceObserver((list) => {
    const entry = list.getEntries()[0];
    vitals.FID = entry.processingStart - entry.startTime;
    callback('FID', vitals.FID, getRating('FID', vitals.FID));
  });

  try {
    fidObserver.observe({ type: 'first-input', buffered: true });
  } catch (e) {
    console.warn('FID not supported');
  }

  // Cumulative Layout Shift
  let clsValue = 0;
  const clsObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (!entry.hadRecentInput) {
        clsValue += entry.value;
      }
    }
    vitals.CLS = clsValue;
    callback('CLS', vitals.CLS, getRating('CLS', vitals.CLS));
  });

  try {
    clsObserver.observe({ type: 'layout-shift', buffered: true });
  } catch (e) {
    console.warn('CLS not supported');
  }

  // First Contentful Paint
  const fcpObserver = new PerformanceObserver((list) => {
    const entry = list
      .getEntries()
      .find((e) => e.name === 'first-contentful-paint');
    if (entry) {
      vitals.FCP = entry.startTime;
      callback('FCP', vitals.FCP, getRating('FCP', vitals.FCP));
    }
  });

  try {
    fcpObserver.observe({ type: 'paint', buffered: true });
  } catch (e) {
    console.warn('Paint timing not supported');
  }

  // Time to First Byte
  const navEntry = performance.getEntriesByType('navigation')[0];
  if (navEntry) {
    vitals.TTFB = navEntry.responseStart;
    callback('TTFB', vitals.TTFB, getRating('TTFB', vitals.TTFB));
  }

  function getRating(metric, value) {
    const thresholds = {
      LCP: [2500, 4000],
      FID: [100, 300],
      CLS: [0.1, 0.25],
      FCP: [1800, 3000],
      TTFB: [800, 1800],
    };

    const [good, poor] = thresholds[metric] || [0, 0];

    if (value <= good) return 'good';
    if (value <= poor) return 'needs-improvement';
    return 'poor';
  }

  return {
    getVitals: () => ({ ...vitals }),
    disconnect: () => {
      lcpObserver.disconnect();
      fidObserver.disconnect();
      clsObserver.disconnect();
      fcpObserver.disconnect();
    },
  };
}

// ============================================
// RESOURCE TIMING ANALYSIS
// ============================================

/**
 * Analyze resource loading performance
 */
class ResourceAnalyzer {
  constructor() {
    this.resources = [];

    this.observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.processEntry(entry);
      }
    });
  }

  start() {
    try {
      this.observer.observe({ type: 'resource', buffered: true });
    } catch (e) {
      // Fallback for older browsers
      this.observer.observe({ entryTypes: ['resource'] });
    }
  }

  processEntry(entry) {
    const resource = {
      name: entry.name,
      type: this.getResourceType(entry),
      duration: entry.duration,
      transferSize: entry.transferSize,
      decodedBodySize: entry.decodedBodySize,
      timing: {
        dns: entry.domainLookupEnd - entry.domainLookupStart,
        tcp: entry.connectEnd - entry.connectStart,
        ssl:
          entry.secureConnectionStart > 0
            ? entry.connectEnd - entry.secureConnectionStart
            : 0,
        ttfb: entry.responseStart - entry.requestStart,
        download: entry.responseEnd - entry.responseStart,
      },
      cached: entry.transferSize === 0 && entry.decodedBodySize > 0,
    };

    this.resources.push(resource);
  }

  getResourceType(entry) {
    const initiatorType = entry.initiatorType;
    const url = entry.name;

    if (
      initiatorType === 'img' ||
      /\.(jpg|jpeg|png|gif|svg|webp)$/i.test(url)
    ) {
      return 'image';
    }
    if (initiatorType === 'script' || /\.js$/i.test(url)) {
      return 'script';
    }
    if (initiatorType === 'link' || /\.css$/i.test(url)) {
      return 'stylesheet';
    }
    if (/\.(woff2?|ttf|eot)$/i.test(url)) {
      return 'font';
    }
    if (initiatorType === 'fetch' || initiatorType === 'xmlhttprequest') {
      return 'api';
    }
    return 'other';
  }

  getReport() {
    const byType = {};
    let totalSize = 0;
    let totalDuration = 0;

    this.resources.forEach((resource) => {
      if (!byType[resource.type]) {
        byType[resource.type] = {
          count: 0,
          totalSize: 0,
          totalDuration: 0,
          cachedCount: 0,
        };
      }

      byType[resource.type].count++;
      byType[resource.type].totalSize += resource.transferSize || 0;
      byType[resource.type].totalDuration += resource.duration;
      if (resource.cached) byType[resource.type].cachedCount++;

      totalSize += resource.transferSize || 0;
      totalDuration += resource.duration;
    });

    return {
      totalResources: this.resources.length,
      totalSize: this.formatBytes(totalSize),
      byType: Object.entries(byType).map(([type, data]) => ({
        type,
        count: data.count,
        size: this.formatBytes(data.totalSize),
        avgDuration: (data.totalDuration / data.count).toFixed(2) + 'ms',
        cacheHitRate: ((data.cachedCount / data.count) * 100).toFixed(1) + '%',
      })),
      slowest: this.resources
        .sort((a, b) => b.duration - a.duration)
        .slice(0, 5)
        .map((r) => ({ name: r.name, duration: r.duration.toFixed(2) + 'ms' })),
      largest: this.resources
        .filter((r) => r.transferSize > 0)
        .sort((a, b) => b.transferSize - a.transferSize)
        .slice(0, 5)
        .map((r) => ({ name: r.name, size: this.formatBytes(r.transferSize) })),
    };
  }

  formatBytes(bytes) {
    if (bytes === 0) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }

  stop() {
    this.observer.disconnect();
  }

  clear() {
    this.resources = [];
  }
}

// ============================================
// LONG TASK MONITORING
// ============================================

/**
 * Monitor long tasks that block the main thread
 */
function monitorLongTasks(callback) {
  let longTasks = [];

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const task = {
        startTime: entry.startTime,
        duration: entry.duration,
        attribution:
          entry.attribution?.map((attr) => ({
            name: attr.name,
            containerType: attr.containerType,
            containerId: attr.containerId,
            containerSrc: attr.containerSrc,
          })) || [],
      };

      longTasks.push(task);

      if (callback) {
        callback(task);
      }

      // Log warning for very long tasks
      if (entry.duration > 100) {
        console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`);
      }
    }
  });

  try {
    observer.observe({ type: 'longtask', buffered: true });
  } catch (e) {
    console.warn('Long task monitoring not supported');
  }

  return {
    getTasks: () => [...longTasks],
    getStats: () => {
      if (longTasks.length === 0) return null;

      const durations = longTasks.map((t) => t.duration);
      return {
        count: longTasks.length,
        total: durations.reduce((a, b) => a + b, 0).toFixed(2) + 'ms',
        average:
          (durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(2) +
          'ms',
        max: Math.max(...durations).toFixed(2) + 'ms',
        min: Math.min(...durations).toFixed(2) + 'ms',
      };
    },
    clear: () => {
      longTasks = [];
    },
    disconnect: () => observer.disconnect(),
  };
}

// ============================================
// NAVIGATION TIMING
// ============================================

/**
 * Get detailed navigation timing
 */
function getNavigationTiming() {
  const entry = performance.getEntriesByType('navigation')[0];

  if (!entry) {
    return null;
  }

  return {
    // DNS
    dnsLookup: entry.domainLookupEnd - entry.domainLookupStart,

    // Connection
    tcpConnect: entry.connectEnd - entry.connectStart,
    sslHandshake:
      entry.secureConnectionStart > 0
        ? entry.connectEnd - entry.secureConnectionStart
        : 0,

    // Request/Response
    timeToFirstByte: entry.responseStart - entry.requestStart,
    contentDownload: entry.responseEnd - entry.responseStart,

    // DOM Processing
    domInteractive: entry.domInteractive - entry.responseEnd,
    domContentLoaded:
      entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
    domComplete: entry.domComplete - entry.responseEnd,

    // Page Load
    loadEvent: entry.loadEventEnd - entry.loadEventStart,
    totalPageLoad: entry.loadEventEnd - entry.startTime,

    // Transfer
    transferSize: entry.transferSize,
    encodedBodySize: entry.encodedBodySize,
    decodedBodySize: entry.decodedBodySize,

    // Type
    type: entry.type, // navigate, reload, back_forward, prerender
    redirectCount: entry.redirectCount,
  };
}

// ============================================
// PERFORMANCE BUDGET MONITOR
// ============================================

/**
 * Monitor against performance budgets
 */
class PerformanceBudget {
  constructor(budgets = {}) {
    this.budgets = {
      LCP: budgets.LCP || 2500,
      FID: budgets.FID || 100,
      CLS: budgets.CLS || 0.1,
      totalJS: budgets.totalJS || 300000, // 300KB
      totalCSS: budgets.totalCSS || 50000, // 50KB
      totalImages: budgets.totalImages || 500000, // 500KB
      resourceCount: budgets.resourceCount || 50,
      longTasks: budgets.longTasks || 5,
    };

    this.violations = [];
    this.metrics = {};
  }

  start() {
    // Monitor Web Vitals
    monitorWebVitals((metric, value) => {
      this.metrics[metric] = value;
      this.checkBudget(metric, value);
    });

    // Monitor resources
    this.resourceAnalyzer = new ResourceAnalyzer();
    this.resourceAnalyzer.start();

    // Monitor long tasks
    this.longTaskMonitor = monitorLongTasks(() => {
      const taskCount = this.longTaskMonitor.getTasks().length;
      this.checkBudget('longTasks', taskCount);
    });

    // Periodic resource check
    this.checkInterval = setInterval(() => {
      this.checkResourceBudgets();
    }, 5000);
  }

  checkBudget(metric, value) {
    const budget = this.budgets[metric];

    if (budget !== undefined && value > budget) {
      const violation = {
        metric,
        value,
        budget,
        exceeded: value - budget,
        timestamp: Date.now(),
      };

      this.violations.push(violation);
      console.warn(
        `Budget violation: ${metric} is ${value}, budget is ${budget}`
      );
    }
  }

  checkResourceBudgets() {
    const resources = this.resourceAnalyzer.resources;

    // Check by type
    const byType = {
      script: 0,
      stylesheet: 0,
      image: 0,
    };

    resources.forEach((r) => {
      if (byType[r.type] !== undefined) {
        byType[r.type] += r.transferSize || 0;
      }
    });

    this.checkBudget('totalJS', byType.script);
    this.checkBudget('totalCSS', byType.stylesheet);
    this.checkBudget('totalImages', byType.image);
    this.checkBudget('resourceCount', resources.length);
  }

  getReport() {
    return {
      budgets: this.budgets,
      metrics: this.metrics,
      violations: this.violations,
      status: this.violations.length === 0 ? 'pass' : 'fail',
      summary: Object.entries(this.budgets).map(([metric, budget]) => ({
        metric,
        budget,
        current: this.metrics[metric],
        status: (this.metrics[metric] || 0) <= budget ? 'pass' : 'fail',
      })),
    };
  }

  stop() {
    clearInterval(this.checkInterval);
    this.resourceAnalyzer.stop();
    this.longTaskMonitor.disconnect();
  }
}

// ============================================
// NODE.JS SIMULATION
// ============================================

console.log('=== PerformanceObserver Examples ===');
console.log('Note: PerformanceObserver is a browser API.');
console.log('');

// Simulate some concepts
console.log('Performance Entry Types:');
const entryTypes = [
  'navigation - Page load timing',
  'resource - Resource loading timing',
  'mark - Custom timestamps',
  'measure - Duration between marks',
  'paint - First paint metrics',
  'longtask - Main thread blocking',
  'largest-contentful-paint - LCP metric',
  'layout-shift - CLS metric',
  'first-input - FID metric',
];

entryTypes.forEach((type) => console.log(`  - ${type}`));
console.log('');

console.log('Core Web Vitals Thresholds:');
console.log('  LCP: Good ≤ 2.5s, Poor > 4.0s');
console.log('  FID: Good ≤ 100ms, Poor > 300ms');
console.log('  CLS: Good ≤ 0.1, Poor > 0.25');
console.log('');

// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    basicPerformanceObserver,
    PerformanceTimer,
    monitorWebVitals,
    ResourceAnalyzer,
    monitorLongTasks,
    getNavigationTiming,
    PerformanceBudget,
  };
}
Examples - JavaScript Tutorial | DeepML