DEV Community

Cover image for Your Site Feels Fast, Until It Doesn't: What Devs Miss Without PerformanceNavigationTiming
John Munn
John Munn

Posted on

Your Site Feels Fast, Until It Doesn't: What Devs Miss Without PerformanceNavigationTiming

You’ve optimized your JS bundle, minified your CSS, and lazy-loaded everything that moves. Lighthouse gives you green scores. DevTools says you're fine. But users are still complaining the site feels slow.

So what gives?

The real issue? Many teams still rely on metrics that don’t reflect what users actually feel, things like outdated timing APIs, synthetic tests, and guesswork stitched together post-mortem.

The browser already knows exactly how long things took. You just need to ask it the right way. That’s where the PerformanceNavigationTiming API comes in, giving you a modern, precise, and reliable view into what your users are actually experiencing.

So how do you move past guesswork and into something measurable?

What is PerformanceNavigationTiming?

It’s the modern way to measure how a page actually loads from the browser's point of view.

It lives inside the Navigation Timing Level 2 spec, and provides a high-resolution, single-object summary of every major milestone in the page lifecycle.

Instead of messing with performance.timing, you can now do this:

performance.timing was verbose, required manual calculations, and returned inconsistent results across browsers. It felt like trying to read a receipt from a thermal printer that had been in someone’s wallet for six months.

const [navigation] = performance.getEntriesByType("navigation");
console.log(navigation);
Enter fullscreen mode Exit fullscreen mode

Clean, modern, and no manual math required.


Why You Should Use It

PerformanceNavigationTiming gives you:

  • Microsecond-level accuracy
  • Clean, consistent access to domContentLoadedloadEventEndresponseStart, and more
  • A full timeline of page load phases
  • Support across all major modern browsers

Lighthouse gives you important baseline data, but it can't tell you if your site feels slow to someone on crowded WiFi with 20 browser extensions. That's where PerformanceNavigationTiming shines, it measures real user experience in production.


When to Use It

  • Real User Monitoring (RUM)
  • Feature rollouts where load performance matters
  • Tracking deploy regressions
  • Internal dashboards or performance observability

If your business depends on speed, you need this data.


What Metrics Can You Extract?

This API is basically your browser’s flight data recorder:

const [nav] = performance.getEntriesByType("navigation");

console.log("Time to First Byte:", nav.responseStart - nav.startTime);
console.log("DOM Loaded:", nav.domContentLoadedEventEnd - nav.startTime);
console.log("Full Load:", nav.loadEventEnd - nav.startTime);
Enter fullscreen mode Exit fullscreen mode

Other useful fields include:

  • type: navigation type (navigatereload, etc.)
  • transferSize: how much data was transferred
  • decodedBodySize: uncompressed size of the response body

Pair this with first-contentful-paint and largest-contentful-paint (via PerformanceObserver) for a holistic view.


Real-World Usage

Here’s how to integrate it in a modern app. We’ll include basic error handling too.

window.addEventListener("load", () => {
  const [nav] = performance.getEntriesByType("navigation");

  fetch("/perf-metrics", {
    method: "POST",
    keepalive: true,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      ttfb: nav.responseStart - nav.startTime,
      domLoad: nav.domContentLoadedEventEnd - nav.startTime,
      fullLoad: nav.loadEventEnd - nav.startTime,
    })
  });
});
Enter fullscreen mode Exit fullscreen mode

You can also use navigator.sendBeacon() for less intrusive delivery.

Here’s a slightly more robust version with basic retry logic:

function sendMetrics(data, retries = 1) {
  fetch("/perf-metrics", {
    method: "POST",
    keepalive: true,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data)
  }).catch((err) => {
    if (retries > 0) {
      setTimeout(() => sendMetrics(data, retries - 1), 1000);
    } else {
      console.warn("Failed to send performance metrics", err);
    }
  });
}

window.addEventListener("load", () => {
  const [nav] = performance.getEntriesByType("navigation");
  sendMetrics({
    ttfb: nav.responseStart - nav.startTime,
    domLoad: nav.domContentLoadedEventEnd - nav.startTime,
    fullLoad: nav.loadEventEnd - nav.startTime,
  });
});
Enter fullscreen mode Exit fullscreen mode

Edge Cases and Gotchas

  • Metrics may be 0 or missing if loaded from bfcache or pre-rendered
  • Works only on full-page navigation (not SPA route changes)
  • Data is only available in the active session; you must read it before unload

For SPAs, you’ll need a hybrid approach with PerformanceObserver and your router. NavigationTiming only captures full-page loads, so for route-based performance insights, you'll need to observe paint and interaction timing manually at each route change. Many devs also pair this with web-vitals or custom observers to track Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS) on transitions.


Wrap-Up: Don’t Guess, Measure

Real performance isn’t about synthetic scores. It’s about all those moments your users feel the site drag, right after you thought you did everything right. Remember the bundle you optimized and the CSS you trimmed? This is how you actually find out if it worked. It’s about real users, real data, and real time.

If you want to build fast apps, measure what "fast" actually means in the browser.

PerformanceNavigationTiming is how.

But only if you use it early, often, and in production.

If you're already using a framework like React or Next.js, consider wrapping the collection logic in a custom hook or integrating it with your existing analytics layer. Your users are already generating performance data, you just need to catch it before it disappears.

Top comments (0)