Skip to content

Commit 2ec1c76

Browse files
authoredJan 22, 2025··
Fireperf web vitals (#8644)
* Add support for capturing web vitals metrics in Firebase performance for Web (Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift) * Modifies export to use sendBeacon instead of fetch API, and shifts the upload time to the first time the page is hidden or unloaded.
1 parent 3c1559b commit 2ec1c76

14 files changed

+522
-207
lines changed
 

‎.changeset/kind-dingos-work.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/performance': minor
3+
'firebase': minor
4+
---
5+
6+
Collect web vital metrics (INP,CLS,LCP) as part of page load event.

‎packages/performance/package.json

+4-7
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
},
1515
"./package.json": "./package.json"
1616
},
17-
"files": [
18-
"dist"
19-
],
17+
"files": ["dist"],
2018
"scripts": {
2119
"lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
2220
"lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
@@ -42,7 +40,8 @@
4240
"@firebase/installations": "0.6.12",
4341
"@firebase/util": "1.10.3",
4442
"@firebase/component": "0.6.12",
45-
"tslib": "^2.1.0"
43+
"tslib": "^2.1.0",
44+
"web-vitals": "^4.2.4"
4645
},
4746
"license": "Apache-2.0",
4847
"devDependencies": {
@@ -62,9 +61,7 @@
6261
},
6362
"typings": "dist/src/index.d.ts",
6463
"nyc": {
65-
"extension": [
66-
".ts"
67-
],
64+
"extension": [".ts"],
6865
"reportDir": "./coverage/node"
6966
}
7067
}

‎packages/performance/src/constants.ts

+9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export const FIRST_CONTENTFUL_PAINT_COUNTER_NAME = '_fcp';
3333

3434
export const FIRST_INPUT_DELAY_COUNTER_NAME = '_fid';
3535

36+
export const LARGEST_CONTENTFUL_PAINT_METRIC_NAME = '_lcp';
37+
export const LARGEST_CONTENTFUL_PAINT_ATTRIBUTE_NAME = 'lcp_element';
38+
39+
export const INTERACTION_TO_NEXT_PAINT_METRIC_NAME = '_inp';
40+
export const INTERACTION_TO_NEXT_PAINT_ATTRIBUTE_NAME = 'inp_interactionTarget';
41+
42+
export const CUMULATIVE_LAYOUT_SHIFT_METRIC_NAME = '_cls';
43+
export const CUMULATIVE_LAYOUT_SHIFT_ATTRIBUTE_NAME = 'cls_largestShiftTarget';
44+
3645
export const CONFIG_LOCAL_STORAGE_KEY = '@firebase/performance/config';
3746

3847
export const CONFIG_EXPIRY_LOCAL_STORAGE_KEY =

‎packages/performance/src/resources/trace.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ import {
2222
OOB_TRACE_PAGE_LOAD_PREFIX,
2323
FIRST_PAINT_COUNTER_NAME,
2424
FIRST_CONTENTFUL_PAINT_COUNTER_NAME,
25-
FIRST_INPUT_DELAY_COUNTER_NAME
25+
FIRST_INPUT_DELAY_COUNTER_NAME,
26+
LARGEST_CONTENTFUL_PAINT_METRIC_NAME,
27+
LARGEST_CONTENTFUL_PAINT_ATTRIBUTE_NAME,
28+
INTERACTION_TO_NEXT_PAINT_METRIC_NAME,
29+
INTERACTION_TO_NEXT_PAINT_ATTRIBUTE_NAME,
30+
CUMULATIVE_LAYOUT_SHIFT_METRIC_NAME,
31+
CUMULATIVE_LAYOUT_SHIFT_ATTRIBUTE_NAME
2632
} from '../constants';
2733
import { Api } from '../services/api_service';
28-
import { logTrace } from '../services/perf_logger';
34+
import { logTrace, flushLogs } from '../services/perf_logger';
2935
import { ERROR_FACTORY, ErrorCode } from '../utils/errors';
3036
import {
3137
isValidCustomAttributeName,
@@ -37,6 +43,7 @@ import {
3743
} from '../utils/metric_utils';
3844
import { PerformanceTrace } from '../public_types';
3945
import { PerformanceController } from '../controllers/perf';
46+
import { CoreVitalMetric, WebVitalMetrics } from './web_vitals';
4047

4148
const enum TraceState {
4249
UNINITIALIZED = 1,
@@ -279,6 +286,7 @@ export class Trace implements PerformanceTrace {
279286
performanceController: PerformanceController,
280287
navigationTimings: PerformanceNavigationTiming[],
281288
paintTimings: PerformanceEntry[],
289+
webVitalMetrics: WebVitalMetrics,
282290
firstInputDelay?: number
283291
): void {
284292
const route = Api.getInstance().getUrl();
@@ -340,7 +348,43 @@ export class Trace implements PerformanceTrace {
340348
}
341349
}
342350

351+
this.addWebVitalMetric(
352+
trace,
353+
LARGEST_CONTENTFUL_PAINT_METRIC_NAME,
354+
LARGEST_CONTENTFUL_PAINT_ATTRIBUTE_NAME,
355+
webVitalMetrics.lcp
356+
);
357+
this.addWebVitalMetric(
358+
trace,
359+
CUMULATIVE_LAYOUT_SHIFT_METRIC_NAME,
360+
CUMULATIVE_LAYOUT_SHIFT_ATTRIBUTE_NAME,
361+
webVitalMetrics.cls
362+
);
363+
this.addWebVitalMetric(
364+
trace,
365+
INTERACTION_TO_NEXT_PAINT_METRIC_NAME,
366+
INTERACTION_TO_NEXT_PAINT_ATTRIBUTE_NAME,
367+
webVitalMetrics.inp
368+
);
369+
370+
// Page load logs are sent at unload time and so should be logged and
371+
// flushed immediately.
343372
logTrace(trace);
373+
flushLogs();
374+
}
375+
376+
static addWebVitalMetric(
377+
trace: Trace,
378+
metricKey: string,
379+
attributeKey: string,
380+
metric?: CoreVitalMetric
381+
): void {
382+
if (metric) {
383+
trace.putMetric(metricKey, Math.floor(metric.value * 1000));
384+
if (metric.elementAttribution) {
385+
trace.putAttribute(attributeKey, metric.elementAttribution);
386+
}
387+
}
344388
}
345389

346390
static createUserTimingTrace(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright 2024 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
export interface CoreVitalMetric {
19+
value: number;
20+
elementAttribution?: string;
21+
}
22+
23+
export interface WebVitalMetrics {
24+
cls?: CoreVitalMetric;
25+
inp?: CoreVitalMetric;
26+
lcp?: CoreVitalMetric;
27+
}

‎packages/performance/src/services/api_service.ts

+14
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
import { ERROR_FACTORY, ErrorCode } from '../utils/errors';
1919
import { isIndexedDBAvailable, areCookiesEnabled } from '@firebase/util';
2020
import { consoleLogger } from '../utils/console_logger';
21+
import {
22+
CLSMetricWithAttribution,
23+
INPMetricWithAttribution,
24+
LCPMetricWithAttribution,
25+
onCLS as vitalsOnCLS,
26+
onINP as vitalsOnINP,
27+
onLCP as vitalsOnLCP
28+
} from 'web-vitals/attribution';
2129

2230
declare global {
2331
interface Window {
@@ -47,6 +55,9 @@ export class Api {
4755
private readonly PerformanceObserver: typeof PerformanceObserver;
4856
private readonly windowLocation: Location;
4957
readonly onFirstInputDelay?: (fn: (fid: number) => void) => void;
58+
readonly onLCP: (fn: (metric: LCPMetricWithAttribution) => void) => void;
59+
readonly onINP: (fn: (metric: INPMetricWithAttribution) => void) => void;
60+
readonly onCLS: (fn: (metric: CLSMetricWithAttribution) => void) => void;
5061
readonly localStorage?: Storage;
5162
readonly document: Document;
5263
readonly navigator: Navigator;
@@ -68,6 +79,9 @@ export class Api {
6879
if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) {
6980
this.onFirstInputDelay = window.perfMetrics.onFirstInputDelay;
7081
}
82+
this.onLCP = vitalsOnLCP;
83+
this.onINP = vitalsOnINP;
84+
this.onCLS = vitalsOnCLS;
7185
}
7286

7387
getUrl(): string {

0 commit comments

Comments
 (0)
Please sign in to comment.