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 13 commits into
base: v5
Choose a base branch
from
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 to measure Early Hints consistently ([#566](https://github.com/GoogleChrome/web-vitals/pull/566))
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
- 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
10 changes: 9 additions & 1 deletion src/attribution/onLCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,15 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => {
.getEntriesByType('resource')
.filter((e) => e.name === lcpEntry.url)[0];

const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);
const ttfb = Math.max(
0,
// From Chrome 115 until, Chrome reported responseStart as the document
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
// bytes, rather than Early Hint bytes. Prefer the Early Hint bytes
// (firstInterimResponseStart) for consistency with other browers, if
// non-zero
navigationEntry.firstInterimResponseStart ||
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
navigationEntry.responseStart - activationStart,
);

const lcpRequestStart = Math.max(
ttfb,
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) {
// From Chrome 115 until, 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 ||
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
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
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
firstInterimResponseStart?: number;
finalResponseHeadersStart?: number;
}

// https://wicg.github.io/event-timing/#sec-performance-event-timing
Expand Down
38 changes: 38 additions & 0 deletions test/e2e/onTTFB-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,44 @@ 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',
'/test/ttfb?earlyHintsDelay=50&attribution=1',
);

const ttfb = await getTTFBBeacon();

if ('finalResponseHeadersStart' in ttfb.attribution.navigationEntry) {
assert.strictEqual(
ttfb.value,
ttfb.attribution.navigationEntry.responseStart,
);
assert.strictEqual(
ttfb.value,
ttfb.attribution.navigationEntry.firstInterimResponseStart,
);
assert(
ttfb.value <
ttfb.attribution.navigationEntry.finalResponseHeadersStart,
);
} else if (
'firstInterimResponseStart' in ttfb.attribution.navigationEntry
) {
// TODO: Can remove these after Chrome 133 lands and above is used.
assert(ttfb.value < ttfb.attribution.navigationEntry.responseStart);
assert.strictEqual(
ttfb.value,
ttfb.attribution.navigationEntry.firstInterimResponseStart,
);
} else {
assert.strictEqual(
ttfb.value,
ttfb.attribution.navigationEntry.responseStart,
);
}
});
});
});

Expand Down
43 changes: 42 additions & 1 deletion test/views/ttfb.njk
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,54 @@
<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')),
enumerable: true,
writeable: true,
});
}

function block(blockingTime) {
const startTime = performance.now();
while (performance.now() < startTime + blockingTime) {
// Block...
}
}

if (params.has('earlyHintsDelay')) {
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
const earlyHintsDelay = Number(params.get('earlyHintsDelay'))
// 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')));
// Chrome >= 133 has both finalResponseHeadersStart and firstInterimResponseStart
if ('finalResponseHeadersStart' in PerformanceNavigationTiming.prototype) {
Object.defineProperties(navEntry, {
'firstInterimResponseStart': {
value: Number(navEntry.responseStart),
enumerable: true,
},
'finalResponseHeadersStart': {
value: Number(navEntry.responseStart + earlyHintsDelay),
enumerable: true,
}
});
} else if ('firstInterimResponseStart' in PerformanceNavigationTiming.prototype) {
// TODO: Can remove these after Chrome 133 lands and above is used.
Object.defineProperties(navEntry, {
'firstInterimResponseStart': {
value: Number(navEntry.responseStart),
enumerable: true,
},
'responseStart': {
value: Number(navEntry.responseStart + earlyHintsDelay),
enumerable: true,
},
})
};
}
</script>

<script type="module">
Expand Down
Loading