Skip to Content
LoadingEvent Processing Time

Event processing time

Chrome 57+ Edge 79+ Firefox 58+ Safari 15+ Details

Analyzes the time spent in each phase of page navigation, from initial redirect to the load event. This helps identify bottlenecks in the page loading process and understand where time is being spent.

Key phases in page load

Page load timeline in milliseconds: the network phases run from 0 to 350 (redirect, DNS lookup, TCP connection, SSL/TLS, request and response), then the processing phases run from 350 to 820 (DOM processing, DOMContentLoaded, resources loading, load event)NetworkRedirectDNS LookupTCP ConnectionSSL/TLSRequest/ResponseProcessingDOM ProcessingDOMContentLoadedResources LoadingLoad Event0100200300400500600700800Time (ms)
Page load timeline in milliseconds: the network phases run from 0 to 350 (redirect, DNS lookup, TCP connection, SSL/TLS, request and response), then the processing phases run from 350 to 820 (DOM processing, DOMContentLoaded, resources loading, load event)NetworkRedirectDNS LookupTCP ConnectionSSL/TLSRequest/ResponseProcessingDOM ProcessingDOMContentLoadedResources LoadingLoad Event0200400600800Time (ms)

What each phase means

PhaseDescriptionImpact
RedirectTime following HTTP redirectsEach redirect adds latency
DNS LookupDomain name resolutionSlow DNS affects all requests
TCP ConnectionEstablishing connectionGeographic distance matters
SSL/TLSSecure handshakeCertificate chain complexity
Request/ResponseServer processing timeBackend performance
DOM ProcessingParsing HTML, building DOMHTML complexity
DOMContentLoadedDOM ready, executing deferred scriptsScript execution time
Load EventAll resources loadedTotal page weight

Snippet

Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.

// Event Processing Time - Analyze page load phases
// https://webperf-snippets.nucliweb.net

(() => {
  const phases = [
    { name: "Redirect", start: "redirectStart", end: "redirectEnd", icon: "🔄" },
    { name: "DNS Lookup", start: "domainLookupStart", end: "domainLookupEnd", icon: "🔍" },
    { name: "TCP Connection", start: "connectStart", end: "connectEnd", icon: "🔌" },
    { name: "SSL/TLS", start: "secureConnectionStart", end: "connectEnd", icon: "🔒" },
    { name: "Request", start: "requestStart", end: "responseStart", icon: "📤" },
    { name: "Response", start: "responseStart", end: "responseEnd", icon: "📥" },
    { name: "DOM Processing", start: "responseEnd", end: "domContentLoadedEventStart", icon: "🏗️" },
    { name: "DOMContentLoaded", start: "domContentLoadedEventStart", end: "domContentLoadedEventEnd", icon: "📄" },
    { name: "Resource Loading", start: "domContentLoadedEventEnd", end: "loadEventStart", icon: "📦" },
    { name: "Load Event", start: "loadEventStart", end: "loadEventEnd", icon: "✅" },
  ];

  new PerformanceObserver((list) => {
    const [entry] = list.getEntries();

    // loadEventEnd is 0 until the load event finishes: the phases would be meaningless
    if (!entry.loadEventEnd) {
      console.log("%c⏳ The page is still loading. Run this snippet again after the load event.", "color: #f59e0b;");
      return;
    }

    const formatMs = (ms) => ms.toFixed(2) + " ms";
    const formatBar = (ms, total) => {
      const pct = total > 0 ? (ms / total) * 100 : 0;
      const width = Math.min(Math.round(pct / 2.5), 40);
      return "█".repeat(width) + "░".repeat(Math.max(0, 20 - width));
    };

    // Calculate all phase durations
    const phaseData = phases
      .map((phase) => {
        const start = entry[phase.start] || 0;
        const end = entry[phase.end] || 0;
        let duration = end - start;

        // Handle SSL which shares end with TCP
        if (phase.name === "SSL/TLS" && entry.secureConnectionStart > 0) {
          duration = entry.connectEnd - entry.secureConnectionStart;
        } else if (phase.name === "TCP Connection" && entry.secureConnectionStart > 0) {
          duration = entry.secureConnectionStart - entry.connectStart;
        }

        return {
          ...phase,
          start,
          end,
          duration: Math.max(0, duration),
        };
      })
      .filter((p) => p.duration > 0 || p.name === "Redirect");

    const totalTime = entry.loadEventEnd - entry.startTime;
    const networkTime = (entry.responseEnd || 0) - entry.startTime;
    const processingTime = totalTime - networkTime;

    // Rating based on total load time
    let rating, color;
    if (totalTime <= 2500) {
      rating = "Fast";
      color = "#22c55e";
    } else if (totalTime <= 4000) {
      rating = "Moderate";
      color = "#f59e0b";
    } else {
      rating = "Slow";
      color = "#ef4444";
    }

    console.group(
      `%c⏱️ Page Load: ${totalTime.toFixed(0)}ms (${rating})`,
      `color: ${color}; font-weight: bold; font-size: 14px;`
    );

    // Summary
    console.log("");
    console.log("%cSummary:", "font-weight: bold;");
    console.log(`   Network time: ${networkTime.toFixed(0)}ms (${((networkTime / totalTime) * 100).toFixed(0)}%)`);
    console.log(`   Processing time: ${processingTime.toFixed(0)}ms (${((processingTime / totalTime) * 100).toFixed(0)}%)`);
    console.log(`   Total: ${totalTime.toFixed(0)}ms`);

    // Timeline visualization
    console.log("");
    console.log("%cTimeline:", "font-weight: bold;");

    phaseData.forEach(({ name, duration, icon }) => {
      const pct = (duration / totalTime) * 100;
      const bar = formatBar(duration, totalTime);
      const durationStr = formatMs(duration).padStart(12);
      const pctStr = `${pct.toFixed(1)}%`.padStart(7);

      // Color code long phases
      let phaseColor = "";
      if (duration > totalTime * 0.3) {
        phaseColor = "color: #ef4444;"; // Red for >30%
      } else if (duration > totalTime * 0.15) {
        phaseColor = "color: #f59e0b;"; // Orange for >15%
      }

      console.log(
        `%c${icon} ${name.padEnd(18)} ${durationStr} ${pctStr}  ${bar}`,
        phaseColor
      );
    });

    // Key milestones
    console.log("");
    console.log("%cKey Milestones:", "font-weight: bold;");
    console.log(`   TTFB (Time to First Byte): ${Math.max(0, entry.responseStart - (entry.activationStart || 0)).toFixed(0)}ms`);
    console.log(`   DOMContentLoaded: ${entry.domContentLoadedEventEnd.toFixed(0)}ms`);
    console.log(`   Load Complete: ${entry.loadEventEnd.toFixed(0)}ms`);

    // Find bottleneck
    const longestPhase = phaseData.reduce((a, b) =>
      a.duration > b.duration ? a : b
    );

    if (longestPhase.duration > totalTime * 0.25) {
      console.log("");
      console.log("%c💡 Bottleneck Detected:", "color: #3b82f6; font-weight: bold;");
      console.log(
        `   ${longestPhase.name} takes ${((longestPhase.duration / totalTime) * 100).toFixed(0)}% of total load time`
      );

      const tips = {
        "Redirect": "Minimize redirects. Update links to final URLs.",
        "DNS Lookup": "Use DNS prefetching (<link rel='dns-prefetch'>). Consider faster DNS.",
        "TCP Connection": "Enable HTTP/2 or HTTP/3. Use connection preconnect.",
        "SSL/TLS": "Enable TLS 1.3. Optimize certificate chain. Use session resumption.",
        "Request": "Optimize server response time. Add caching headers.",
        "Response": "Enable compression. Reduce HTML size. Use streaming.",
        "DOM Processing": "Simplify HTML structure. Defer non-critical scripts.",
        "DOMContentLoaded": "Reduce synchronous JavaScript. Use defer/async.",
        "Resource Loading": "Lazy load images. Prioritize critical resources.",
        "Load Event": "Reduce page weight. Optimize resource loading order.",
      };

      console.log(`   Tip: ${tips[longestPhase.name] || "Investigate this phase."}`);
    }

    console.groupEnd();
  }).observe({ type: "navigation", buffered: true });

  // Synchronous return for agent
  const [navSync] = performance.getEntriesByType("navigation");
  if (!navSync) return { script: "Event-Processing-Time", status: "error", error: "No navigation entry" };
  if (!navSync.loadEventEnd) {
    return {
      script: "Event-Processing-Time",
      status: "tracking",
      message: "The page is still loading. Run this snippet again after the load event.",
    };
  }
  const totalTimeSync = navSync.loadEventEnd - navSync.startTime;
  const networkTimeSync = (navSync.responseEnd || 0) - navSync.startTime;
  const ratingSync = totalTimeSync <= 2500 ? "good" : totalTimeSync <= 4000 ? "needs-improvement" : "poor";
  const phasesSync = [
    { key: "redirect", duration: Math.max(0, navSync.redirectEnd - navSync.redirectStart) },
    { key: "dnsLookup", duration: Math.max(0, navSync.domainLookupEnd - navSync.domainLookupStart) },
    { key: "tcpConnection", duration: Math.max(0, (navSync.secureConnectionStart > 0 ? navSync.secureConnectionStart : navSync.connectEnd) - navSync.connectStart) },
    { key: "sslTls", duration: navSync.secureConnectionStart > 0 ? Math.max(0, navSync.connectEnd - navSync.secureConnectionStart) : 0 },
    { key: "request", duration: Math.max(0, navSync.responseStart - navSync.requestStart) },
    { key: "response", duration: Math.max(0, navSync.responseEnd - navSync.responseStart) },
    { key: "domProcessing", duration: Math.max(0, navSync.domContentLoadedEventStart - navSync.responseEnd) },
    { key: "domContentLoaded", duration: Math.max(0, navSync.domContentLoadedEventEnd - navSync.domContentLoadedEventStart) },
    { key: "resourceLoading", duration: Math.max(0, navSync.loadEventStart - navSync.domContentLoadedEventEnd) },
    { key: "loadEvent", duration: Math.max(0, navSync.loadEventEnd - navSync.loadEventStart) },
  ];
  const nonZeroPhases = phasesSync.filter((p) => p.duration > 0);
  const slowestPhase = nonZeroPhases.length > 0 ? nonZeroPhases.reduce((a, b) => a.duration > b.duration ? a : b) : phasesSync[0];
  return {
    script: "Event-Processing-Time",
    status: "ok",
    value: Math.round(totalTimeSync),
    unit: "ms",
    rating: ratingSync,
    thresholds: { good: 2500, needsImprovement: 4000 },
    details: {
      networkTimeMs: Math.round(networkTimeSync),
      processingTimeMs: Math.round(totalTimeSync - networkTimeSync),
      ttfbMs: Math.round(Math.max(0, navSync.responseStart - (navSync.activationStart || 0))),
      domContentLoadedMs: Math.round(navSync.domContentLoadedEventEnd),
      loadCompleteMs: Math.round(navSync.loadEventEnd),
      phases: Object.fromEntries(phasesSync.map((p) => [p.key, { value: Math.round(p.duration), unit: "ms" }])),
      slowestPhase: slowestPhase.key,
    },
  };
})();

Understanding the results

The snippet displays a timeline of all page load phases with:

Summary section

  • Network time: Time spent on network operations (DNS, TCP, SSL, request/response)
  • Processing time: Time spent on DOM parsing and JavaScript execution
  • Total: Complete page load time

Timeline section

Each phase shows:

  • Duration in milliseconds
  • Percentage of total load time
  • Visual bar indicating relative duration
  • Color coding: Red (>30%), Orange (>15%), default otherwise

Key milestones

  • TTFB: When the first byte of HTML arrives
  • DOMContentLoaded: When HTML is parsed and deferred scripts executed
  • Load Complete: When all resources (images, styles, etc.) are loaded

Bottleneck detection

If any phase takes more than 25% of total load time, the snippet identifies it and provides optimization tips.

Run the snippet after the load event. While the page is still loading it reports tracking (and logs a reminder) instead of a total.

Typical values

MetricGoodModerateSlow
Total Load Time< 2.5s2.5s - 4s> 4s
TTFB< 800ms800ms - 1800ms> 1800ms
DOMContentLoaded< 1.5s1.5s - 3s> 3s

Browser support

FeatureChromeEdgeFirefoxSafari
PerformanceNavigationTiming (opens in a new tab) 57 12 58 15
PerformanceObserver (opens in a new tab) 52 79 57 11
All of the above 57 79 58 15

Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.

Further reading

Last updated on
MIT 2026 © Joan León | @nucliweb