Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interoperable TTFB to measure Early Hints consistently #566

Open
wants to merge 8 commits into
base: v5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- **[BREAKING]** Remove the deprecated `onFID()` function ([#519](https://github.com/GoogleChrome/web-vitals/pull/519))
- **[BREAKING]** Change browser support policy to Baseline Widely Available ([#525](https://github.com/GoogleChrome/web-vitals/pull/525))
- **[BREAKING]** Sort the classes that appear in attribution selectors to reduce cardinality ([#518](https://github.com/GoogleChrome/web-vitals/pull/518))
- **[BREAKING]** Add interoperable TTFB and `documentDuration` to measure Early Hints better ([#566](https://github.com/GoogleChrome/web-vitals/pull/566))
- Cap INP breakdowns to INP duration ([#528](https://github.com/GoogleChrome/web-vitals/pull/528))
- Cap LCP load duration to LCP time ([#527](https://github.com/GoogleChrome/web-vitals/pull/527))

Expand Down
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;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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
Loading