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);
Clean, modern, and no manual math required.
Why You Should Use It
PerformanceNavigationTiming
gives you:
- Microsecond-level accuracy
- Clean, consistent access to
domContentLoaded
,loadEventEnd
,responseStart
, 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);
Other useful fields include:
-
type
: navigation type (navigate
,reload
, 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,
})
});
});
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,
});
});
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)