Navigation Timing API and CDN: Measuring Real User Performance
Synthetic tests tell you how fast your CDN could be. The Navigation Timing API tells you how fast it actually is for real users — across every edge location, device, and network condition. For any team running a CDN in production, this API is the foundation of trustworthy Real User Monitoring (RUM).
Without it, you are guessing. Cache hit ratios from your CDN dashboard show edge efficiency, but they do not show what the user experienced. A 98% cache hit ratio means little if Time to First Byte (TTFB) is still 600ms for users in Southeast Asia due to TLS negotiation overhead or origin shielding misconfiguration. The Navigation Timing API closes that gap by exposing high-resolution, browser-native timing for every navigation, directly from the user's perspective.
What the Navigation Timing API Actually Measures
The Navigation Timing API provides a detailed timeline of a page navigation, from the moment the user initiates it to when the page is fully loaded. It is exposed via performance.getEntriesByType('navigation') and returns a PerformanceNavigationTiming object. Unlike the deprecated performance.timing, it offers nanosecond precision, is not affected by clock skew, and integrates cleanly with modern Performance Timeline and Resource Timing.
Every timestamp is a DOMHighResTimeStamp relative to timeOrigin, measured from the start of navigation. This eliminates negative values and cross-browser inconsistencies that plagued Navigation Timing Level 1. For CDN analysis, this precision matters because edge performance differences are often in the 5-50ms range — invisible to low-resolution timers but critical at scale.
Level 1 vs. Level 2: Why It Matters for CDN Monitoring
If you still see code referencing performance.timing.navigationStart, it is obsolete. Navigation Timing Level 1 used absolute epoch timestamps and had a single global object. Level 2, now standard in all modern browsers, provides:
- High-resolution relative timestamps: All values are offsets from
startTime (0), avoiding system clock adjustments. - PerformanceNavigationTiming interface: Extends
PerformanceResourceTiming, so you get DNS, TCP, TLS, request/response, and transfer sizes in one entry. - Worker and back-forward cache support: Correctly handles
activationStartfor prerendered pages andpageshowevents. - Secure context protections: Cross-origin timings are zeroed unless
Timing-Allow-Originis set — crucial when your CDN serves third-party assets.
For RUM, always use Level 2. Feature detection is straightforward and prevents silent data corruption from old implementations.
Engineer Note: Do not mix performance.timing and PerformanceNavigationTiming in the same dataset. The two use different clocks and epochs. If you must support legacy browsers, normalize everything to relative durations on the client before beaconing.
The CDN-Critical Metrics Inside PerformanceNavigationTiming
Not every attribute is useful for CDN evaluation. These six are where CDN configuration directly shows up in user data:
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
1. redirectStart / redirectEnd: Captures CDN-level redirects, such as HTTP to HTTPS upgrades at the edge or geo-based routing. A persistent 30-80ms redirect duration often indicates you should enforce HSTS and handle redirects at the edge, not the origin.
2. domainLookupStart / domainLookupEnd (DNS): Measures DNS resolution time. With a CDN, this should be consistently low (5-20ms) due to Anycast and short TTLs. Spikes suggest DNS provider issues or missing prefetch hints.
3. connectStart / connectEnd and secureConnectionStart (TCP + TLS): This is where HTTP/2, HTTP/3 and QUIC performance becomes visible. connectEnd - connectStart is TCP handshake time. connectEnd - secureConnectionStart is TLS negotiation. On a well-tuned CDN with TLS 1.3, 0-RTT, and session resumption, TLS should be 0-40ms. Values above 100ms point to absent session tickets or distant edge nodes.
4. requestStart / responseStart (TTFB): The gold standard for CDN performance. responseStart - requestStart is network TTFB, while responseStart - startTime is user-perceived TTFB. For cached content, this should be 20-60ms globally. For dynamic or uncached content, it reveals origin shielding and Argo-style routing effectiveness.
5. responseEnd / transferSize / encodedBodySize: Indicates payload delivery efficiency. Compare transferSize vs encodedBodySize to verify Brotli/Gzip at the edge, and responseEnd - responseStart to detect slow last-mile networks vs. edge throughput bottlenecks.
6. domContentLoadedEventStart / loadEventEnd: While not CDN-specific, they correlate CDN timing with actual user experience. A fast TTFB that does not improve LCP suggests your CDN is fast but your origin HTML is render-blocking.
To make these metrics actionable, you need to correlate them with CDN cache status. Most CDNs expose this via a response header like CF-Cache-Status, X-Cache, or Server-Timing. Reading that header via PerformanceResourceTiming.serverTiming or a custom response header is essential for segmenting RUM data by HIT, MISS, BYPASS, and EXPIRED.
Mapping Navigation Timing to CDN Performance Diagnostics
Raw timings are noise until you segment them. The table below shows how to interpret each interval and what CDN action it triggers. Use it as a triage checklist when your RUM dashboard alerts on p95 TTFB regression.
| Interval (Calculation) | What It Measures | Healthy CDN Range | CDN Diagnosis If High |
|---|---|---|---|
redirectEnd - redirectStart |
Edge redirect latency | 0 ms (HSTS) | Missing edge rule; force HTTPS at edge, preload HSTS |
domainLookupEnd - domainLookupStart |
DNS lookup | 5–20 ms | Non-Anycast DNS, long TTL, no preconnect hint |
connectEnd - connectStart |
TCP handshake | 10–40 ms | Distant PoP, no Anycast, TCP tuning needed |
connectEnd - secureConnectionStart |
TLS negotiation | 0–40 ms (TLS 1.3) | No session resumption, TLS 1.2 fallback, missing 0-RTT |
responseStart - requestStart |
TTFB (network) | 15–60 ms HIT, 80–250 ms MISS | Low cache hit ratio, no origin shield, slow origin |
responseEnd - responseStart |
Content download | Depends on size; <100ms for <100KB | No compression, HTTP/1.1, congested edge, large payload |
domContentLoadedEventStart - responseEnd |
HTML parsing to DCL | <300 ms | Render-blocking JS/CSS, not CDN but impacts perceived CDN speed |
For a complete view of how these timings fit into your overall observability strategy, correlate them with the core CDN performance metrics you already collect at the edge — especially cache hit ratio, origin response time, and edge CPU time.
Implementing RUM with Navigation Timing and CDN Headers
Collecting Navigation Timing correctly requires handling single-page navigations, bfcache restores, and sampling to avoid beacon floods. The pattern below is production-tested and CDN-aware: it captures cache status via Server-Timing and falls back to a custom header if your CDN does not yet emit Server-Timing.
1. Basic Navigation Timing Capture
// Capture after load to ensure all timings are finalized
function getNavigationMetrics() {
const [nav] = performance.getEntriesByType('navigation');
if (!nav) return null;
return {
ttfb: nav.responseStart - nav.requestStart, // network TTFB
ttfbUser: nav.responseStart - nav.startTime, // user-perceived TTFB
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
tls: nav.secureConnectionStart ? nav.connectEnd - nav.secureConnectionStart : 0,
download: nav.responseEnd - nav.responseStart,
total: nav.loadEventEnd - nav.startTime,
transferSize: nav.transferSize,
encodedBodySize: nav.encodedBodySize,
cacheStatus: null // filled below
};
}
window.addEventListener('load', () => {
// Use setTimeout to wait for loadEventEnd to be populated
setTimeout(() => {
const m = getNavigationMetrics();
if (m) navigator.sendBeacon('/rum', JSON.stringify(m));
}, 0);
});2. Correlating with CDN Cache Status
Without cache segmentation, your TTFB histogram is meaningless — HITs and MISSes have fundamentally different distributions. Modern CDNs expose cache state via the Server-Timing header, which is automatically parsed into performance.getEntriesByType('navigation')[0].serverTiming.
// Read CDN cache status from Server-Timing (preferred) or ResourceTiming
function getCacheStatus(navEntry) {
// Example: Server-Timing: cdn-cache;desc=HIT, edge;dur=12
if (navEntry.serverTiming && navEntry.serverTiming.length) {
const hit = navEntry.serverTiming.find(e => e.name === 'cdn-cache');
if (hit) return hit.description || 'HIT'; // HIT, MISS, BYPASS
}
// Fallback: requires Timing-Allow-Origin + custom header exposure
return navEntry.nextHopProtocol || 'unknown';
}
// For cross-origin CDN assets, ensure CDN sends:
// Timing-Allow-Origin: * (or your origin)
// Server-Timing: cdn-cache;desc=HITWhy Server-Timing Beats X-Cache Headers
X-Cache or CF-Cache-Status are not exposed to JavaScript unless you add Access-Control-Expose-Headers. Server-Timing is explicitly designed for performance observability and is parsed natively by the browser. It also lets you emit multiple metrics — cache status, edge duration, origin duration, and shield status — without extra requests. If your CDN supports it, standardize on it.
For strategies on improving the HIT rate you will measure, review your CDN caching strategies — especially cache-key normalization and stale-while-revalidate.
3. Handling bfcache and Prerender
Back-forward cache restores do not trigger a full navigation. If you only listen to load, you will undercount fast navigations and skew your p50. Listen to pageshow and check event.persisted, and use performance.getEntriesByType('navigation')[0].activationStart to detect prerendered pages where startTime is artificially early.
From Measurement to Action: Optimizing Your CDN
Once you have segmented RUM, patterns emerge quickly. A typical global RUM dataset shows a bimodal TTFB distribution: a tight cluster at 25-45ms (HIT) and a long tail at 180-400ms (MISS). Your job is to shrink both the tail and the distance between modes.
If HIT TTFB is high (>80ms p75): The edge itself is slow. Check TLS configuration — enforce TLS 1.3, enable 0-RTT where safe, and ensure session tickets are shared across PoPs. Evaluate HTTP/3 and QUIC for lossy networks; Navigation Timing will show nextHopProtocol: h3 and reduced connectEnd on retransmitted networks. Also verify your CDN's Anycast routing — a user in Berlin should not be routed to London due to BGP misconfiguration.
If MISS TTFB is high (>300ms p75): Origin shielding is failing. Enable tiered caching so only one PoP per region hits origin. Reduce origin TTFB by colocating origin near your shield PoP and using persistent connections (keep-alive) between edge and origin. Navigation Timing cannot directly see origin time, but Server-Timing: origin;dur=210 emitted by your CDN will. Correlate responseStart - requestStart with that value to isolate edge vs. origin latency.
If DNS + TCP is high but TTFB is normal: Users are paying connection overhead on every navigation. Add <link rel="preconnect" href="https://cdn.example.com"> and dns-prefetch for third-party CDN domains, and ensure your CDN uses Anycast with EDNS Client Subnet support for accurate geo-routing.
If download duration is high: This is often not a CDN PoP issue but a payload issue. Verify Brotli compression at the edge (content-encoding: br), enable range requests for large assets, and audit transferSize vs. decodedBodySize. A sudden increase in transferSize without a code deploy often means compression was disabled by a cache invalidation that bypassed the compression layer.
RUM Architecture: Sampling, Beacons, and Privacy
Do not beacon 100% of navigations. At scale, this creates both cost and performance overhead. Sample at 5-10% for high-traffic sites, 100% for low-traffic, and always sample 100% of slow experiences (TTFB > 500ms) to preserve tail visibility. Use navigator.sendBeacon() or fetch(..., {keepalive: true}) to avoid blocking unload, and batch metrics with PerformanceObserver for SPA navigations.
Privacy matters. Navigation Timing itself contains no PII, but when combined with IP-based geo and URL paths, it can become identifying. Truncate URLs to path templates, avoid beaconing full query strings with tokens, and respect navigator.doNotTrack if your policy requires it. For GDPR, ensure your RUM endpoint is covered by your DPA and that beacons are sent to the same CDN edge to avoid extra cross-border transfers.
Finally, do not treat Navigation Timing as a replacement for synthetic monitoring. Synthetics give you controlled, repeatable tests from known locations with full waterfall details. RUM gives you the real distribution. Use synthetics to detect regressions in staging and RUM to validate them in production. When both agree that p95 TTFB dropped 30ms after enabling Tiered Cache, you have a credible win to report.
Quick Audit Checklist: 1) Are you using Level 2 (PerformanceNavigationTiming)? 2) Are you segmenting by serverTiming cache status? 3) Are you handling bfcache? 4) Are you sampling correctly? 5) Are you correlating TTFB with nextHopProtocol to track HTTP/3 adoption? If any answer is no, fix it before optimizing cache rules.
When implemented correctly, the Navigation Timing API turns every real user visit into a precise CDN probe — without extra infrastructure. It is the most honest measure of whether your CDN is actually delivering on its promise of faster, more reliable performance.
Frequently Asked Questions
How does Navigation Timing API distinguish CDN cache HIT vs. MISS?
The API itself does not know about CDN caching. You must correlate Navigation Timing with CDN headers. The best practice is to have your CDN emit Server-Timing: cdn-cache;desc=HIT (or MISS/BYPASS) and read it via performance.getEntriesByType('navigation')[0].serverTiming. Then segment your TTFB (responseStart - requestStart) by that value. HITs should cluster at 15–60ms; MISSes will be 3–8x higher depending on origin distance.
Why is my Navigation Timing TTFB different from curl or synthetic tests?
Real User Monitoring includes last-mile network, DNS, TCP/TLS negotiation, and device contention that synthetics often bypass. Synthetics typically run from well-connected data centers with warm DNS and reused connections. RUM measures cold-start, first-visit performance on real networks. Use requestStart to responseStart for network TTFB and compare it to responseStart - startTime (which includes queuing) to isolate network vs. browser overhead.
Can Navigation Timing API measure HTTP/3 and QUIC improvements?
Yes, indirectly. Check performance.getEntriesByType('navigation')[0].nextHopProtocol — it will report h3, h2, or http/1.1. Then compare connectEnd - connectStart and TLS durations across protocols. QUIC (HTTP/3) merges TCP and TLS handshakes, so you should see lower connection times and fewer retransmission tails, especially on lossy mobile networks. Segment your RUM by protocol to quantify the gain before rolling out globally.
Do I need Timing-Allow-Origin to use Navigation Timing with a CDN?
For same-origin navigations (your HTML document), no — you get full timing automatically. For cross-origin resources (CDN-hosted images, scripts, APIs) measured via Resource Timing, yes — the CDN must send Timing-Allow-Origin: * or your origin, otherwise DNS/TCP/TLS timings are zeroed for privacy. For the main navigation itself, ensure your CDN does not strip Server-Timing headers and that they are exposed if you read them via JavaScript.
Tags: Navigation Timing API CDN Performance RUM Web Performance