Skip to content

Commit

Permalink
Ignore TTFB for loads where responseStart is zero
Browse files Browse the repository at this point in the history
  • Loading branch information
philipwalton committed Nov 14, 2022
1 parent 049e669 commit c3cd48e
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 9 deletions.
23 changes: 15 additions & 8 deletions src/onTTFB.ts
Expand Up @@ -67,21 +67,28 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => {
const navEntry = getNavigationEntry();

if (navEntry) {
// 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(navEntry.responseStart - getActivationStart(), 0);
const responseStart = navEntry.responseStart;

// In some cases the value reported is negative or is larger
// than the current page time. Ignore these cases:
// https://github.com/GoogleChrome/web-vitals/issues/137
// https://github.com/GoogleChrome/web-vitals/issues/162
if (metric.value < 0 || metric.value > performance.now()) return;
if (responseStart < 0 || responseStart > performance.now()) return;

metric.entries = [navEntry];
// If the navigation entry's `responseStart` value is 0, ignore it.
// This likely means the request included a cross-origin redirect, and
// the browser has removed timing info for privacy/security reasons.
// See: https://github.com/GoogleChrome/web-vitals/issues/275
if (responseStart > 0) {
// 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(responseStart - getActivationStart(), 0);

report(true);
metric.entries = [navEntry];
report(true);
}

// Only report TTFB after bfcache restores if a `navigation` entry
// was reported for the initial load.
Expand Down
29 changes: 28 additions & 1 deletion test/e2e/onTTFB-test.js
Expand Up @@ -16,6 +16,7 @@

import assert from 'assert';
import {beaconCountIs, clearBeacons, getBeacons} from '../utils/beacons.js';
import {domReadyState} from '../utils/domReadyState.js';
import {stubForwardBack} from '../utils/stubForwardBack.js';


Expand Down Expand Up @@ -175,15 +176,41 @@ describe('onTTFB()', async function() {

const ttfb2 = await getTTFBBeacon();

assert(ttfb2.value >= 0);
assert(ttfb2.id.match(/^v3-\d+-\d+$/));
assert.strictEqual(ttfb2.value, 0);
assert.strictEqual(ttfb2.name, 'TTFB');
assert.strictEqual(ttfb2.value, ttfb2.delta);
assert.strictEqual(ttfb2.rating, 'good');
assert.strictEqual(ttfb2.navigationType, 'back-forward-cache');
assert.strictEqual(ttfb2.entries.length, 0);
});

it('ignores navigations with no responseStart timestamp', async function() {
await browser.url('/test/ttfb?responseStart=0');

await domReadyState('complete');

// Wait a bit to ensure no beacons were sent.
await browser.pause(1000);

const loadBeacons = await getBeacons();
assert.strictEqual(loadBeacons.length, 0);

// Test back-forward navigations to ensure they're sent, even if the
// initial page TTFB value is ignored.
await stubForwardBack();

const ttfb = await getTTFBBeacon();

assert(ttfb.id.match(/^v3-\d+-\d+$/));
assert.strictEqual(ttfb.value, 0);
assert.strictEqual(ttfb.name, 'TTFB');
assert.strictEqual(ttfb.value, ttfb.delta);
assert.strictEqual(ttfb.rating, 'good');
assert.strictEqual(ttfb.navigationType, 'back-forward-cache');
assert.strictEqual(ttfb.entries.length, 0);
});

describe('attribution', function() {
it('includes attribution data on the metric object', async function() {
await browser.url('/test/ttfb?attribution=1');
Expand Down
12 changes: 12 additions & 0 deletions test/views/ttfb.njk
Expand Up @@ -24,6 +24,18 @@

<p><a id="navigate-away" href="https://example.com">Navigate away</a></p>

<script>
// Set the blocking values based on query params if present.
const params = new URLSearchParams(location.search);
if (params.has('responseStart')) {
const navEntry = performance.getEntriesByType('navigation')[0];
Object.defineProperty(navEntry, 'responseStart', {
value: Number(params.get('responseStart')),
});
}
</script>

<script type="module">
import {onTTFB} from '{{ modulePath }}';
Expand Down

0 comments on commit c3cd48e

Please sign in to comment.