Server-Timing and Early Hints viewer
Lists the Server-Timing metrics that the server sends with the document and with each resource, and reports the 103 Early Hints timing of the navigation. These two signals show where the backend spends time before the first byte and whether the server lets the browser start fetching critical resources while it still builds the response.
Why this matters
TTFB shows that the server is slow, not why. A Server-Timing header lets the backend report its own phases (database, cache lookup, rendering, edge) so they show up in DevTools and in the Performance API next to the network timings. Early Hints (103) lets the server send Link: rel=preload or rel=preconnect headers before the final response is ready, which hides part of the server think time behind useful downloads.
What it reads
| Source | Property | Meaning |
|---|---|---|
| Navigation entry | serverTiming | Metrics the document response reports |
| Resource entries | serverTiming | Metrics each subresource response reports |
| Navigation entry | firstInterimResponseStart | When the first interim response (103 Early Hints) arrived |
| Navigation entry | finalResponseHeadersStart | When the headers of the final response arrived |
| Navigation entry | responseStart | When the first byte of the final response arrived |
Each Server-Timing metric has a name, an optional dur (duration in milliseconds) and an optional desc (description). A metric without dur is reported with a duration of 0.
Snippet
// Server-Timing & Early Hints Viewer
// https://webperf-snippets.nucliweb.net
(() => {
const SCRIPT = "Server-Timing-Early-Hints";
const MAX_ITEMS = 50;
const [nav] = performance.getEntriesByType("navigation");
if (!nav) {
return { script: SCRIPT, status: "error", error: "No navigation entry available" };
}
if (!Array.isArray(nav.serverTiming)) {
return {
script: SCRIPT,
status: "unsupported",
error: "Server-Timing entries are not supported in this browser",
};
}
const round = (n) => Math.round(n * 100) / 100;
// Times are relative to the time origin; a prerendered page starts at activationStart
const activationStart = Math.max(0, nav.activationStart ?? 0);
const rel = (t) => (t > 0 ? round(Math.max(0, t - activationStart)) : 0);
const toItem = (entry, source, url) => ({
source,
url,
name: entry.name,
durationMs: round(entry.duration || 0),
description: entry.description || "",
});
// Server-Timing entries of the document
const all = nav.serverTiming.map((m) => toItem(m, "navigation", location.href));
// Server-Timing entries of the resources. A cross-origin response without
// Timing-Allow-Origin exposes no Server-Timing and reports zero for every size, so it is
// counted as unreadable instead of being treated as a resource with no metrics.
let resourcesWithServerTiming = 0;
let corsRestrictedCount = 0;
for (const r of performance.getEntriesByType("resource")) {
if (r.serverTiming.length > 0) {
resourcesWithServerTiming++;
for (const m of r.serverTiming) all.push(toItem(m, "resource", r.name));
continue;
}
let crossOrigin = false;
try {
crossOrigin = new URL(r.name).origin !== location.origin;
} catch {
// Not a valid URL; treat as same-origin
}
if (crossOrigin && r.transferSize === 0 && r.encodedBodySize === 0 && r.decodedBodySize === 0) {
corsRestrictedCount++;
}
}
all.sort((a, b) => b.durationMs - a.durationMs);
const items = all.slice(0, MAX_ITEMS);
// 103 Early Hints. firstInterimResponseStart is 0 when the server sent no interim
// response, or when the browser did not expose it (Early Hints needs HTTP/2 or later).
const firstInterim = rel(nav.firstInterimResponseStart ?? 0);
const finalHeaders = rel(nav.finalResponseHeadersStart ?? nav.responseStart);
const earlyHintsReceived = firstInterim > 0;
const navigation = {
responseStartMs: rel(nav.responseStart),
firstInterimResponseStartMs: firstInterim,
finalResponseHeadersStartMs: finalHeaders,
earlyHintsReceived,
earlyHintsLeadMs: earlyHintsReceived ? round(Math.max(0, finalHeaders - firstInterim)) : 0,
serverTimingCount: nav.serverTiming.length,
};
const issues = [];
if (nav.serverTiming.length === 0) {
issues.push({
severity: "info",
message:
"The document response sends no Server-Timing header. Add one, for example Server-Timing: db;dur=53, to expose backend timings in DevTools and in the Performance API.",
});
}
if (corsRestrictedCount > 0) {
issues.push({
severity: "info",
message: `${corsRestrictedCount} cross-origin resource(s) hide their timing because they lack Timing-Allow-Origin, so their Server-Timing cannot be read. Add Timing-Allow-Origin to expose it.`,
});
}
if (!earlyHintsReceived) {
issues.push({
severity: "info",
message:
"No 103 Early Hints response was observed for the document. If the server can send Link preload or preconnect headers before the final response, Early Hints lets the browser start those fetches earlier.",
});
}
// Human output
console.group("%cServer-Timing & Early Hints", "font-weight: bold; font-size: 14px;");
console.log(
`Document: ${nav.serverTiming.length} Server-Timing metric(s); resources with Server-Timing: ${resourcesWithServerTiming}`
);
console.log(
earlyHintsReceived
? `103 Early Hints at ${firstInterim} ms, final headers at ${finalHeaders} ms (${navigation.earlyHintsLeadMs} ms lead)`
: `No 103 Early Hints observed; final headers at ${finalHeaders} ms`
);
if (corsRestrictedCount > 0) {
console.log(`${corsRestrictedCount} cross-origin resource(s) unreadable (no Timing-Allow-Origin)`);
}
if (items.length > 0) console.table(items);
console.groupEnd();
return {
script: SCRIPT,
status: "ok",
count: all.length,
corsLimitedAnalysis: corsRestrictedCount > 0,
details: {
navigation,
resourcesWithServerTiming,
corsRestrictedCount,
},
items,
issues,
};
})();
Understanding the results
The snippet prints a summary and a table in the console, and returns a structured object.
Metrics
items holds up to 50 metrics, slowest first, with the fields source (navigation or resource), url, name, durationMs and description. count is the total number of metrics found, including those beyond the first 50.
Early Hints timing
details.navigation reports:
firstInterimResponseStartMs: when the 103 response arrived, or0when none was observed.finalResponseHeadersStartMs: when the final response headers arrived.earlyHintsLeadMs: how much earlier the browser learned about the hinted resources than it would have without Early Hints.earlyHintsReceived:truewhen a 103 response was observed.
Times are relative to the start of the navigation, or to activationStart on a prerendered page.
firstInterimResponseStart value.Cross-origin resources
A cross-origin resource exposes its Server-Timing metrics only when its response sends a Timing-Allow-Origin header that permits the page origin. Without that header the browser reports an empty serverTiming list and zero for every size, so the snippet cannot tell a resource with no metrics from a hidden one. It counts the resources that look hidden (cross-origin with zero sizes) in details.corsRestrictedCount, sets corsLimitedAnalysis to true and adds an issue. details.resourcesWithServerTiming counts the resources whose metrics were read.
Adding Server-Timing to a response
Server-Timing: db;dur=53.2;desc="Database", cache;desc=HIT, total;dur=120Timing-Allow-Origin: https://example.comUse a short metric name and do not expose internal details, since any script on the page can read these values.
Sending Early Hints
HTTP/2 103
Link: </styles/app.css>; rel=preload; as=style
Link: <https://cdn.example.com>; rel=preconnectSend the same Link headers only for resources that every response for the URL needs, such as the main stylesheet or the LCP image.
Browser support
serverTiming is available in Chromium, Firefox and Safari. firstInterimResponseStart and finalResponseHeadersStart are Chromium features. Where a property is missing the snippet reports 0 for it.
Further reading
- Server-Timing (opens in a new tab) | MDN
- Timing-Allow-Origin (opens in a new tab) | MDN
- Early Hints (opens in a new tab) | Chrome Developers
- Optimize TTFB (opens in a new tab) | web.dev
- TTFB sub-parts | Break the time to first byte into its phases