CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-web-vitals

Easily measure performance metrics in JavaScript

Pending
Overview
Eval results
Files

attribution.mddocs/

Attribution Build

Enhanced measurement functions that include detailed attribution data for performance debugging and optimization. The attribution build provides the same API as the standard build but with additional diagnostic information to help identify the root cause of performance issues.

Overview

The attribution build is slightly larger (~1.5K additional, brotli'd) but provides invaluable debugging information. Each metric callback receives a MetricWithAttribution object instead of a regular Metric object, containing an additional attribution property with diagnostic details.

Import Pattern

// Standard build
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";

// Attribution build - same function names, enhanced data
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";

Capabilities

CLS Attribution

Provides detailed information about layout shifts, including the specific elements that caused shifts and their impact.

/**
 * CLS measurement with attribution data for debugging layout shifts
 * @param callback - Function to receive CLS metric with attribution data
 * @param opts - Optional configuration including custom target generation
 */
function onCLS(callback: (metric: CLSMetricWithAttribution) => void, opts?: AttributionReportOpts): void;

interface CLSMetricWithAttribution extends CLSMetric {
  attribution: CLSAttribution;
}

interface CLSAttribution {
  /** Selector for the first element that shifted in the largest layout shift */
  largestShiftTarget?: string;
  /** Time when the single largest layout shift occurred */
  largestShiftTime?: DOMHighResTimeStamp;
  /** Layout shift score of the single largest shift */
  largestShiftValue?: number;
  /** The LayoutShift entry representing the largest shift */
  largestShiftEntry?: LayoutShift;
  /** First element source from the largestShiftEntry sources list */
  largestShiftSource?: LayoutShiftAttribution;
  /** Loading state when the largest layout shift occurred */
  loadState?: LoadState;
}

Usage Example:

import { onCLS } from "web-vitals/attribution";

onCLS((metric) => {
  console.log('CLS score:', metric.value);
  console.log('Worst shift element:', metric.attribution.largestShiftTarget);
  console.log('Worst shift value:', metric.attribution.largestShiftValue);
  console.log('Page state during shift:', metric.attribution.loadState);
  
  // Send detailed data for analysis
  analytics.track('cls_issue', {
    value: metric.value,
    element: metric.attribution.largestShiftTarget,
    shiftValue: metric.attribution.largestShiftValue,
    loadState: metric.attribution.loadState
  });
});

INP Attribution

Provides detailed information about the slowest interaction, including timing breakdown and responsible elements.

/**
 * INP measurement with attribution data for debugging slow interactions
 * @param callback - Function to receive INP metric with attribution data
 * @param opts - Optional configuration including duration threshold
 */
function onINP(callback: (metric: INPMetricWithAttribution) => void, opts?: INPAttributionReportOpts): void;

interface INPMetricWithAttribution extends INPMetric {
  attribution: INPAttribution;
}

interface INPAttribution {
  /** CSS selector of the element that received the interaction */
  interactionTarget: string;
  /** Time when the user first interacted */
  interactionTime: DOMHighResTimeStamp;
  /** Type of interaction ('pointer' | 'keyboard') */
  interactionType: 'pointer' | 'keyboard';
  /** Best-guess timestamp of the next paint after interaction */
  nextPaintTime: DOMHighResTimeStamp;
  /** Event timing entries processed within the same animation frame */
  processedEventEntries: PerformanceEventTiming[];
  /** Time from interaction start to processing start */
  inputDelay: number;
  /** Time spent processing the interaction */
  processingDuration: number;
  /** Time from processing end to next paint */
  presentationDelay: number;
  /** Loading state when interaction occurred */
  loadState: LoadState;
  /** Long animation frame entries intersecting the interaction */
  longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[];
  /** Summary of the longest script intersecting the INP duration */
  longestScript?: INPLongestScriptSummary;
  /** Total duration of Long Animation Frame scripts intersecting INP */
  totalScriptDuration?: number;
  /** Total style and layout duration from Long Animation Frames */
  totalStyleAndLayoutDuration?: number;
  /** Off main-thread presentation delay */
  totalPaintDuration?: number;
  /** Total unattributed time not included in other totals */
  totalUnattributedDuration?: number;
}

interface INPLongestScriptSummary {
  /** The longest Long Animation Frame script entry intersecting the INP interaction */
  entry: PerformanceScriptTiming;
  /** The INP subpart where the longest script ran */
  subpart: 'input-delay' | 'processing-duration' | 'presentation-delay';
  /** The amount of time the longest script intersected the INP duration */
  intersectingDuration: number;
}

Usage Example:

import { onINP } from "web-vitals/attribution";

onINP((metric) => {
  console.log('INP time:', metric.value + 'ms');
  console.log('Slow interaction target:', metric.attribution.interactionTarget);
  console.log('Interaction type:', metric.attribution.interactionType);
  
  // Timing breakdown
  console.log('Input delay:', metric.attribution.inputDelay + 'ms');
  console.log('Processing time:', metric.attribution.processingDuration + 'ms');
  console.log('Presentation delay:', metric.attribution.presentationDelay + 'ms');
  
  // Identify bottleneck
  const bottleneck = Math.max(
    metric.attribution.inputDelay,
    metric.attribution.processingDuration,
    metric.attribution.presentationDelay
  );
  
  if (bottleneck === metric.attribution.processingDuration) {
    console.log('Bottleneck: JavaScript processing time');
  } else if (bottleneck === metric.attribution.presentationDelay) {
    console.log('Bottleneck: Rendering/paint time');
  } else {
    console.log('Bottleneck: Input delay');
  }
});

LCP Attribution

Provides detailed information about the largest contentful paint element and loading phases.

/**
 * LCP measurement with attribution data for debugging loading performance
 * @param callback - Function to receive LCP metric with attribution data
 * @param opts - Optional configuration including custom target generation
 */
function onLCP(callback: (metric: LCPMetricWithAttribution) => void, opts?: AttributionReportOpts): void;

interface LCPMetricWithAttribution extends LCPMetric {
  attribution: LCPAttribution;
}

interface LCPAttribution {
  /** CSS selector of the LCP element */
  target?: string;
  /** URL of the LCP resource (for images) */
  url?: string;
  /** Time from page load initiation to first byte received (TTFB) */
  timeToFirstByte: number;
  /** Delta between TTFB and when browser starts loading LCP resource */
  resourceLoadDelay: number;
  /** Total time to load the LCP resource itself */
  resourceLoadDuration: number;
  /** Delta from resource load finish to LCP element fully rendered */
  elementRenderDelay: number;
  /** Navigation entry for diagnosing general page load issues */
  navigationEntry?: PerformanceNavigationTiming;
  /** Resource entry for the LCP resource for diagnosing load issues */
  lcpResourceEntry?: PerformanceResourceTiming;
}

Usage Example:

import { onLCP } from "web-vitals/attribution";

onLCP((metric) => {
  console.log('LCP time:', metric.value + 'ms');
  console.log('LCP element:', metric.attribution.element);
  console.log('LCP resource URL:', metric.attribution.url);
  
  // Loading phase breakdown
  console.log('Time to resource start:', metric.attribution.timeToFirstByte + 'ms');
  console.log('Resource load delay:', metric.attribution.resourceLoadDelay + 'ms');
  console.log('Resource load duration:', metric.attribution.resourceLoadDuration + 'ms');
  console.log('Element render delay:', metric.attribution.elementRenderDelay + 'ms');
  
  // Identify optimization opportunities
  if (metric.attribution.resourceLoadDelay > 100) {
    console.log('Consider preloading the LCP resource');
  }
  if (metric.attribution.elementRenderDelay > 50) {
    console.log('Consider optimizing render-blocking resources');
  }
});

FCP Attribution

Provides information about the first contentful paint and potential blocking resources.

/**
 * FCP measurement with attribution data for debugging first paint
 * @param callback - Function to receive FCP metric with attribution data
 * @param opts - Optional configuration including custom target generation
 */
function onFCP(callback: (metric: FCPMetricWithAttribution) => void, opts?: AttributionReportOpts): void;

interface FCPMetricWithAttribution extends FCPMetric {
  attribution: FCPAttribution;
}

interface FCPAttribution {
  /** Time from page load initiation to first byte received (TTFB) */
  timeToFirstByte: number;
  /** Delta between TTFB and first contentful paint */
  firstByteToFCP: number;
  /** Loading state when FCP occurred */
  loadState: LoadState;
  /** PerformancePaintTiming entry corresponding to FCP */
  fcpEntry?: PerformancePaintTiming;
  /** Navigation entry for diagnosing general page load issues */
  navigationEntry?: PerformanceNavigationTiming;
}

TTFB Attribution

Provides detailed breakdown of server response time components.

/**
 * TTFB measurement with attribution data for debugging server response
 * @param callback - Function to receive TTFB metric with attribution data
 * @param opts - Optional configuration including custom target generation
 */
function onTTFB(callback: (metric: TTFBMetricWithAttribution) => void, opts?: AttributionReportOpts): void;

interface TTFBMetricWithAttribution extends TTFBMetric {
  attribution: TTFBAttribution;
}

interface TTFBAttribution {
  /** Total time from user initiation to page start handling request */
  waitingDuration: number;
  /** Total time spent checking HTTP cache for a match */
  cacheDuration: number;
  /** Total time to resolve DNS for the requested domain */
  dnsDuration: number;
  /** Total time to create connection to the requested domain */
  connectionDuration: number;
  /** Time from request sent to first byte received (includes network + server time) */
  requestDuration: number;
  /** Navigation entry for diagnosing general page load issues */
  navigationEntry?: PerformanceNavigationTiming;
}

Usage Example:

import { onTTFB } from "web-vitals/attribution";

onTTFB((metric) => {
  console.log('TTFB time:', metric.value + 'ms');
  
  // Network timing breakdown
  console.log('DNS lookup:', metric.attribution.dnsDuration + 'ms');
  console.log('Connection time:', metric.attribution.connectionDuration + 'ms');
  console.log('Server response:', metric.attribution.requestDuration + 'ms');
  
  // Optimization suggestions
  if (metric.attribution.dnsDuration > 20) {
    console.log('Consider DNS prefetching or using a faster DNS resolver');
  }
  if (metric.attribution.connectionDuration > 100) {
    console.log('Consider using HTTP/2 or reducing connection setup time');
  }
  if (metric.attribution.requestDuration > 600) {
    console.log('Consider server-side optimizations or CDN usage');
  }
});

Configuration Options

Custom Target Generation

The attribution build allows customizing how DOM elements are converted to CSS selectors for debugging.

interface AttributionReportOpts extends ReportOpts {
  /**
   * Custom function to generate target selectors for DOM elements
   * @param el - The DOM element to generate a selector for
   * @returns A string selector, null, or undefined (falls back to default)
   */
  generateTarget?: (el: Node | null) => string | null | undefined;
}

Usage Example:

import { onCLS } from "web-vitals/attribution";

onCLS((metric) => {
  console.log('Custom element selector:', metric.attribution.largestShiftTarget);
}, {
  generateTarget: (element) => {
    // Custom selector generation logic
    if (element?.id) {
      return `#${element.id}`;
    }
    if (element?.className) {
      return `.${element.className.split(' ')[0]}`;
    }
    return element?.tagName?.toLowerCase() || 'unknown';
  }
});

Supported Types

type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete';

interface AttributionReportOpts extends ReportOpts {
  generateTarget?: (el: Node | null) => string | null | undefined;
}

interface INPAttributionReportOpts extends AttributionReportOpts {
  durationThreshold?: number;
}

Install with Tessl CLI

npx tessl i tessl/npm-web-vitals

docs

additional-web-vitals.md

attribution.md

core-web-vitals.md

index.md

thresholds.md

tile.json