diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 687fc0c1..d3125b7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,11 +30,11 @@ To test a subset of browsers or metrics, run the following in separate terminals - `npm run watch` - `npm run test:server` -- `npm run test:e2e -- --browsesr=chrome --metrics=TTFB` +- `npm run test:e2e -- --browsers=chrome --metrics=TTFB` The last command can be replaced as you see fit and include comma, separated values. For example: -- `npm run test:e2e -- --browsesr=chrome,firefox --metrics=TTFB,LCP` +- `npm run test:e2e -- --browsers=chrome,firefox --metrics=TTFB,LCP` To run an individual test, change `it('test name')` to `it.only('test name')`. diff --git a/README.md b/README.md index e1c51a15..a685bffc 100644 --- a/README.md +++ b/README.md @@ -1022,15 +1022,23 @@ interface LCPAttribution { #### TTFB `attribution`: ```ts -interface TTFBAttribution { +export interface TTFBAttribution { /** * The total time from when the user initiates loading the page to when the - * DNS lookup begins. This includes redirects, service worker startup, and - * HTTP cache lookup times. + * page starts to handle the request. Large values here are typically due + * to HTTP redirects, though other browser processing contributes to this + * duration as well (so even without redirect it's generally not zero). */ waitingDuration: number; /** - * The total time to resolve the DNS for the current request. + * The total time spent checking the HTTP cache for a match. For navigations + * handled via service worker, this duration usually includes service worker + * start-up time as well as time processing `fetch` event listeners, with + * some exceptions, see: https://github.com/w3c/navigation-timing/issues/199 + */ + cacheDuration: number; + /** + * The total time to resolve the DNS for the requested domain. */ dnsDuration: number; /** @@ -1045,8 +1053,8 @@ interface TTFBAttribution { requestDuration: 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 example: - * navigationEntry?.serverTiming + * general page load issues. This can be used to access `serverTiming` for + * example: navigationEntry?.serverTiming */ navigationEntry?: PerformanceNavigationTiming; } diff --git a/docs/upgrading-to-v4.md b/docs/upgrading-to-v4.md index 0a1e3102..5cd35d57 100644 --- a/docs/upgrading-to-v4.md +++ b/docs/upgrading-to-v4.md @@ -14,32 +14,33 @@ npm install web-vitals@next #### General -- **Removed** the "base+polyfill" build, which includes the FID polyfill and the Navigation Timing polyfill supporting legacy Safari browsers (see [#435](https://github.com/GoogleChrome/web-vitals/pull/435)). -- **Removed** all `getXXX()` functions that were deprecated in v3 (see [#435](https://github.com/GoogleChrome/web-vitals/pull/435)). +- **Removed** the "base+polyfill" build, which includes the FID polyfill and the Navigation Timing polyfill supporting legacy Safari browsers ([#435](https://github.com/GoogleChrome/web-vitals/pull/435)). +- **Removed** all `getXXX()` functions that were deprecated in v3 ([#435](https://github.com/GoogleChrome/web-vitals/pull/435)). #### `INPMetric` -- **Changed** `entries` to only include entries with matching `interactionId` that were processed within the same animation frame. Previously it included all entries with matching `interactionId` values, which could include entries not impacting INP (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Changed** `entries` to only include entries with matching `interactionId` that were processed within the same animation frame. Previously it included all entries with matching `interactionId` values, which could include entries not impacting INP ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). ### Attribution build #### `INPAttribution` -- **Renamed** `eventTarget` to `interactionTarget` (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Renamed** `eventTime` to `interactionTime` (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Renamed** `eventType` to `interactionType`. Also this property will now always be either "pointer" or "keyboard" (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Removed** `eventEntry` in favor of the new `processedEventEntries` array (see below), which includes all `event` entries processed within the same animation frame as the INP candidate interaction (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Renamed** `eventTarget` to `interactionTarget` ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Renamed** `eventTime` to `interactionTime` ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Renamed** `eventType` to `interactionType`. Also this property will now always be either "pointer" or "keyboard" ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Removed** `eventEntry` in favor of the new `processedEventEntries` array (see below), which includes all `event` entries processed within the same animation frame as the INP candidate interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). #### `LCPAttribution` -- **Renamed** `resourceLoadTime` to `resourceLoadDuration` (see [#450](https://github.com/GoogleChrome/web-vitals/pull/450)). +- **Renamed** `resourceLoadTime` to `resourceLoadDuration` ([#450](https://github.com/GoogleChrome/web-vitals/pull/450)). #### `TTFBAttribution` -- **Renamed** `waitingTime` to `waitingDuration` (see [#453](https://github.com/GoogleChrome/web-vitals/pull/453)). -- **Renamed** `dnsTime` to `dnsDuration` (see [#453](https://github.com/GoogleChrome/web-vitals/pull/453)). -- **Renamed** `connectionTime` to `connectionDuration` (see [#453](https://github.com/GoogleChrome/web-vitals/pull/453)). -- **Renamed** `requestTime` to `requestDuration` (see [#453](https://github.com/GoogleChrome/web-vitals/pull/453)). +- **Removed** `waitingTime` in favor of splitting this duration into `waitingDuration` and `cacheDuration` (see below) ([#458](https://github.com/GoogleChrome/web-vitals/pull/458)). +- **Renamed** `requestTime` to `requestDuration` ([#453](https://github.com/GoogleChrome/web-vitals/pull/453)). +- **Added** `redirectDuration` ([#458](https://github.com/GoogleChrome/web-vitals/pull/458)). +- **Added** `waitingDuration` ([#458](https://github.com/GoogleChrome/web-vitals/pull/458)). +- **Added** `cacheDuration` ([#458](https://github.com/GoogleChrome/web-vitals/pull/458)). ## 🚀 New features @@ -51,9 +52,9 @@ No new features were introduced into the "standard" build, outside of the breaki #### `INPAttribution` -- **Added** `nextPaintTime`, which marks the timestamp of the next paint after the interaction (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Added** `inputDelay`, which measures the time from when the user interacted with the page until when the browser was first able to start processing event listeners for that interaction. (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Added** `processingDuration`, which measures the time from when the first event listener started running in response to the user interaction until when all event listener processing has finished (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Added** `presentationDelay`, which measures the time from when the browser finished processing all event listeners for the user interaction until the next frame is presented on the screen and visible to the user. (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Added** `processedEventEntries`, an array of `event` entries that were processed within the same animation frame as the INP candidate interaction (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). -- **Added** `longAnimationFrameEntries`, which includes any `long-animation-frame` entries that overlap with the INP candidate interaction (see [#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Added** `nextPaintTime`, which marks the timestamp of the next paint after the interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Added** `inputDelay`, which measures the time from when the user interacted with the page until when the browser was first able to start processing event listeners for that interaction. ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Added** `processingDuration`, which measures the time from when the first event listener started running in response to the user interaction until when all event listener processing has finished ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Added** `presentationDelay`, which measures the time from when the browser finished processing all event listeners for the user interaction until the next frame is presented on the screen and visible to the user. ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Added** `processedEventEntries`, an array of `event` entries that were processed within the same animation frame as the INP candidate interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). +- **Added** `longAnimationFrameEntries`, which includes any `long-animation-frame` entries that overlap with the INP candidate interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)). diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index fa99e1cb..7c1876e4 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -28,6 +28,14 @@ const attributeTTFB = (metric: TTFBMetric): void => { const navigationEntry = metric.entries[0]; const activationStart = navigationEntry.activationStart || 0; + // Measure from workerStart or fetchStart so any service worker startup + // time is included in cacheDuration (which also includes other sw time + // anyway, that cannot be accurately split out cross-browser). + const waitEnd = Math.max( + (navigationEntry.workerStart || navigationEntry.fetchStart) - + activationStart, + 0, + ); const dnsStart = Math.max( navigationEntry.domainLookupStart - activationStart, 0, @@ -36,16 +44,23 @@ const attributeTTFB = (metric: TTFBMetric): void => { navigationEntry.connectStart - activationStart, 0, ); - const requestStart = Math.max( - navigationEntry.requestStart - activationStart, + const connectEnd = Math.max( + navigationEntry.connectEnd - activationStart, 0, ); (metric as TTFBMetricWithAttribution).attribution = { - waitingDuration: dnsStart, + waitingDuration: waitEnd, + cacheDuration: dnsStart - waitEnd, + // dnsEnd usually equals connectStart but use connectStart over dnsEnd + // for dnsDuration in case there ever is a gap. dnsDuration: connectStart - dnsStart, - connectionDuration: requestStart - connectStart, - requestDuration: metric.value - requestStart, + connectionDuration: connectEnd - connectStart, + // There is often a gap between connectEnd and requestStart. Attribute + // that to requestDuration so connectionDuration remains 0 for + // service worker controlled requests were connectStart and connectEnd + // are the same. + requestDuration: metric.value - connectEnd, navigationEntry: navigationEntry, }; return; @@ -53,6 +68,7 @@ const attributeTTFB = (metric: TTFBMetric): void => { // Set an empty object if no other attribution has been set. (metric as TTFBMetricWithAttribution).attribution = { waitingDuration: 0, + cacheDuration: 0, dnsDuration: 0, connectionDuration: 0, requestDuration: 0, diff --git a/src/types/ttfb.ts b/src/types/ttfb.ts index 719d4310..2f7097c7 100644 --- a/src/types/ttfb.ts +++ b/src/types/ttfb.ts @@ -28,16 +28,28 @@ export interface TTFBMetric extends Metric { * An object containing potentially-helpful debugging information that * can be sent along with the TTFB value for the current page visit in order * to help identify issues happening to real-users in the field. + * + * NOTE: these values are primarily useful for page loads not handled via + * service worker, as browsers differ in what they report when service worker + * is involved, see: https://github.com/w3c/navigation-timing/issues/199 */ export interface TTFBAttribution { /** * The total time from when the user initiates loading the page to when the - * DNS lookup begins. This includes redirects, service worker startup, and - * HTTP cache lookup times. + * page starts to handle the request. Large values here are typically due + * to HTTP redirects, though other browser processing contributes to this + * duration as well (so even without redirect it's generally not zero). */ waitingDuration: number; /** - * The total time to resolve the DNS for the current request. + * The total time spent checking the HTTP cache for a match. For navigations + * handled via service worker, this duration usually includes service worker + * start-up time as well as time processing `fetch` event listeners, with + * some exceptions, see: https://github.com/w3c/navigation-timing/issues/199 + */ + cacheDuration: number; + /** + * The total time to resolve the DNS for the requested domain. */ dnsDuration: number; /** @@ -52,8 +64,8 @@ export interface TTFBAttribution { requestDuration: 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 example: - * navigationEntry?.serverTiming + * general page load issues. This can be used to access `serverTiming` for + * example: navigationEntry?.serverTiming */ navigationEntry?: PerformanceNavigationTiming; } diff --git a/test/e2e/onTTFB-test.js b/test/e2e/onTTFB-test.js index 95296e1d..01833a78 100644 --- a/test/e2e/onTTFB-test.js +++ b/test/e2e/onTTFB-test.js @@ -262,7 +262,12 @@ describe('onTTFB()', async function () { const navEntry = ttfb.entries[0]; assert.strictEqual( ttfb.attribution.waitingDuration, - navEntry.domainLookupStart, + navEntry.workerStart || navEntry.fetchStart, + ); + assert.strictEqual( + ttfb.attribution.cacheDuration, + navEntry.domainLookupStart - + (navEntry.workerStart || navEntry.fetchStart), ); assert.strictEqual( ttfb.attribution.dnsDuration, @@ -270,11 +275,11 @@ describe('onTTFB()', async function () { ); assert.strictEqual( ttfb.attribution.connectionDuration, - navEntry.requestStart - navEntry.connectStart, + navEntry.connectEnd - navEntry.connectStart, ); assert.strictEqual( ttfb.attribution.requestDuration, - navEntry.responseStart - navEntry.requestStart, + navEntry.responseStart - navEntry.connectEnd, ); assert.deepEqual(ttfb.attribution.navigationEntry, navEntry); @@ -304,7 +309,18 @@ describe('onTTFB()', async function () { const navEntry = ttfb.entries[0]; assert.strictEqual( ttfb.attribution.waitingDuration, - Math.max(0, navEntry.domainLookupStart - activationStart), + Math.max( + 0, + (navEntry.workerStart || navEntry.fetchStart) - activationStart, + ), + ); + assert.strictEqual( + ttfb.attribution.cacheDuration, + Math.max(0, navEntry.domainLookupStart - activationStart) - + Math.max( + 0, + (navEntry.workerStart || navEntry.fetchStart) - activationStart, + ), ); assert.strictEqual( ttfb.attribution.dnsDuration, @@ -313,14 +329,13 @@ describe('onTTFB()', async function () { ); assert.strictEqual( ttfb.attribution.connectionDuration, - Math.max(0, navEntry.requestStart - activationStart) - + Math.max(0, navEntry.connectEnd - activationStart) - Math.max(0, navEntry.connectStart - activationStart), ); - assert.strictEqual( ttfb.attribution.requestDuration, Math.max(0, navEntry.responseStart - activationStart) - - Math.max(0, navEntry.requestStart - activationStart), + Math.max(0, navEntry.connectEnd - activationStart), ); assert.deepEqual(ttfb.attribution.navigationEntry, navEntry); @@ -347,6 +362,7 @@ describe('onTTFB()', async function () { assert.strictEqual(ttfb.entries.length, 0); assert.strictEqual(ttfb.attribution.waitingDuration, 0); + assert.strictEqual(ttfb.attribution.cacheDuration, 0); assert.strictEqual(ttfb.attribution.dnsDuration, 0); assert.strictEqual(ttfb.attribution.connectionDuration, 0); assert.strictEqual(ttfb.attribution.requestDuration, 0);