Skip to content

Commit

Permalink
Add Early Hints support
Browse files Browse the repository at this point in the history
  • Loading branch information
tunetheweb committed Nov 7, 2024
1 parent 807fb2b commit 157b90e
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 6 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,13 @@ export interface TTFBAttribution {
* processing time.
*/
requestDuration: number;
/**
* The total time from the first byte of the response was received. Until the
* first byte of the document was received. This will only be non-zero for
* servers using Early Hints and where browsers support sending this additional
* timing. This time is after the TTFB metric.value.
*/
documentDuration: number;
/**
* The `navigation` entry of the current page, which is useful for diagnosing
* general page load issues. This can be used to access `serverTiming` for
Expand Down
7 changes: 7 additions & 0 deletions src/attribution/onTTFB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => {
dnsDuration: 0,
connectionDuration: 0,
requestDuration: 0,
documentDuration: 0,
};

if (metric.entries.length) {
Expand Down Expand Up @@ -57,6 +58,11 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => {
0,
);

// Fallback to responseStart for finalResponseHeadersStart
const finalResponseHeadersStart =
(navigationEntry.finalResponseHeadersStart ??
navigationEntry.responseStart) - activationStart;

attribution = {
waitingDuration: waitEnd,
cacheDuration: dnsStart - waitEnd,
Expand All @@ -69,6 +75,7 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => {
// service worker controlled requests were connectStart and connectEnd
// are the same.
requestDuration: metric.value - connectEnd,
documentDuration: finalResponseHeadersStart - metric.value,
navigationEntry: navigationEntry,
};
}
Expand Down
13 changes: 9 additions & 4 deletions src/onTTFB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,19 @@ export const onTTFB = (
const navigationEntry = getNavigationEntry();

if (navigationEntry) {
// Form Chrome 115 until Chrome 132 (with flags), Chrome reported
// responseStart as the document bytes, rather than Early Hint bytes.
// Prefer the Early Hint bytes (firstInterimResponseStart) for
// consistency with other browers, if non-zero
const responseStart =
navigationEntry.firstInterimResponseStart ||
navigationEntry.responseStart;

// The activationStart reference is used because TTFB should be
// relative to page activation rather than navigation start if the
// page was prerendered. But in cases where `activationStart` occurs
// after the first byte is received, this time should be clamped at 0.
metric.value = Math.max(
navigationEntry.responseStart - getActivationStart(),
0,
);
metric.value = Math.max(responseStart - getActivationStart(), 0);

metric.entries = [navigationEntry];
report(true);
Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ declare global {
durationThreshold?: number;
}

// https://wicg.github.io/nav-speculation/prerendering.html#performance-navigation-timing-extension
interface PerformanceNavigationTiming {
// https://wicg.github.io/nav-speculation/prerendering.html#performance-navigation-timing-extension
activationStart?: number;
// Early Hints support
firstInterimResponseStart?: number;
finalResponseHeadersStart?: number;
}

// https://wicg.github.io/event-timing/#sec-performance-event-timing
Expand Down
7 changes: 7 additions & 0 deletions src/types/ttfb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export interface TTFBAttribution {
* processing time.
*/
requestDuration: number;
/**
* The total time from the first byte of the response was received. Until the
* first byte of the document was received. This will only be non-zero for
* servers using Early Hints and where browsers support sending this additional
* timing. This time is after the TTFB metric.value.
*/
documentDuration: number;
/**
* The `navigation` entry of the current page, which is useful for diagnosing
* general page load issues. This can be used to access `serverTiming` for
Expand Down
11 changes: 11 additions & 0 deletions test/e2e/onTTFB-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,17 @@ describe('onTTFB()', async function () {
assert.strictEqual(ttfb.attribution.requestDuration, 0);
assert.strictEqual(ttfb.attribution.navigationEntry, undefined);
});

it('reports the correct value for Early Hints', async function () {
await navigateTo(
'/test/ttfb?responseStart=10&earlyHintsDelay=50&attribution=1',
);

const ttfb = await getTTFBBeacon();

assert.strictEqual(ttfb.value, 10);
assert.strictEqual(ttfb.attribution.documentDuration, 50);
});
});
});

Expand Down
28 changes: 27 additions & 1 deletion test/views/ttfb.njk
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,39 @@
<script>
// Set the blocking values based on query params if present.
const params = new URLSearchParams(location.search);
const navEntry = performance.getEntriesByType('navigation')[0];
if (params.has('responseStart')) {
const navEntry = performance.getEntriesByType('navigation')[0];
Object.defineProperty(navEntry, 'responseStart', {
value: Number(params.get('responseStart')),
});
}
function block(blockingTime) {
const startTime = performance.now();
while (performance.now() < startTime + blockingTime) {
// Block...
}
}
if (params.has('earlyHintsDelay')) {
const earlyHintsDelay = Number(params.get('earlyHintsDelay'))
const responseStart = navEntry.responseStart;
// Block for delay time—to avoid the library seeing future timestamps,
// and so not reporting a TTFB at all as it's invalid.
block(Number(params.get('earlyHintsDelay')));
Object.defineProperties(navEntry, {
'firstInterimResponseStart': {
value: Number(responseStart),
},
'finalResponseHeadersStart': {
value: Number(responseStart + earlyHintsDelay),
},
'responseStart': {
value: Number(responseStart + earlyHintsDelay),
}
});
}
</script>

<script type="module">
Expand Down

0 comments on commit 157b90e

Please sign in to comment.