0
# Attribution Build
1
2
Enhanced measurement functions that include detailed attribution data for performance debugging and optimization. The attribution build provides the same API as the standard build but with additional diagnostic information to help identify the root cause of performance issues.
3
4
## Overview
5
6
The attribution build is slightly larger (~1.5K additional, brotli'd) but provides invaluable debugging information. Each metric callback receives a `MetricWithAttribution` object instead of a regular `Metric` object, containing an additional `attribution` property with diagnostic details.
7
8
## Import Pattern
9
10
```typescript
11
// Standard build
12
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
13
14
// Attribution build - same function names, enhanced data
15
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";
16
```
17
18
## Capabilities
19
20
### CLS Attribution
21
22
Provides detailed information about layout shifts, including the specific elements that caused shifts and their impact.
23
24
```typescript { .api }
25
/**
26
* CLS measurement with attribution data for debugging layout shifts
27
* @param callback - Function to receive CLS metric with attribution data
28
* @param opts - Optional configuration including custom target generation
29
*/
30
function onCLS(callback: (metric: CLSMetricWithAttribution) => void, opts?: AttributionReportOpts): void;
31
32
interface CLSMetricWithAttribution extends CLSMetric {
33
attribution: CLSAttribution;
34
}
35
36
interface CLSAttribution {
37
/** Selector for the first element that shifted in the largest layout shift */
38
largestShiftTarget?: string;
39
/** Time when the single largest layout shift occurred */
40
largestShiftTime?: DOMHighResTimeStamp;
41
/** Layout shift score of the single largest shift */
42
largestShiftValue?: number;
43
/** The LayoutShift entry representing the largest shift */
44
largestShiftEntry?: LayoutShift;
45
/** First element source from the largestShiftEntry sources list */
46
largestShiftSource?: LayoutShiftAttribution;
47
/** Loading state when the largest layout shift occurred */
48
loadState?: LoadState;
49
}
50
```
51
52
**Usage Example:**
53
54
```typescript
55
import { onCLS } from "web-vitals/attribution";
56
57
onCLS((metric) => {
58
console.log('CLS score:', metric.value);
59
console.log('Worst shift element:', metric.attribution.largestShiftTarget);
60
console.log('Worst shift value:', metric.attribution.largestShiftValue);
61
console.log('Page state during shift:', metric.attribution.loadState);
62
63
// Send detailed data for analysis
64
analytics.track('cls_issue', {
65
value: metric.value,
66
element: metric.attribution.largestShiftTarget,
67
shiftValue: metric.attribution.largestShiftValue,
68
loadState: metric.attribution.loadState
69
});
70
});
71
```
72
73
### INP Attribution
74
75
Provides detailed information about the slowest interaction, including timing breakdown and responsible elements.
76
77
```typescript { .api }
78
/**
79
* INP measurement with attribution data for debugging slow interactions
80
* @param callback - Function to receive INP metric with attribution data
81
* @param opts - Optional configuration including duration threshold
82
*/
83
function onINP(callback: (metric: INPMetricWithAttribution) => void, opts?: INPAttributionReportOpts): void;
84
85
interface INPMetricWithAttribution extends INPMetric {
86
attribution: INPAttribution;
87
}
88
89
interface INPAttribution {
90
/** CSS selector of the element that received the interaction */
91
interactionTarget: string;
92
/** Time when the user first interacted */
93
interactionTime: DOMHighResTimeStamp;
94
/** Type of interaction ('pointer' | 'keyboard') */
95
interactionType: 'pointer' | 'keyboard';
96
/** Best-guess timestamp of the next paint after interaction */
97
nextPaintTime: DOMHighResTimeStamp;
98
/** Event timing entries processed within the same animation frame */
99
processedEventEntries: PerformanceEventTiming[];
100
/** Time from interaction start to processing start */
101
inputDelay: number;
102
/** Time spent processing the interaction */
103
processingDuration: number;
104
/** Time from processing end to next paint */
105
presentationDelay: number;
106
/** Loading state when interaction occurred */
107
loadState: LoadState;
108
/** Long animation frame entries intersecting the interaction */
109
longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[];
110
/** Summary of the longest script intersecting the INP duration */
111
longestScript?: INPLongestScriptSummary;
112
/** Total duration of Long Animation Frame scripts intersecting INP */
113
totalScriptDuration?: number;
114
/** Total style and layout duration from Long Animation Frames */
115
totalStyleAndLayoutDuration?: number;
116
/** Off main-thread presentation delay */
117
totalPaintDuration?: number;
118
/** Total unattributed time not included in other totals */
119
totalUnattributedDuration?: number;
120
}
121
122
interface INPLongestScriptSummary {
123
/** The longest Long Animation Frame script entry intersecting the INP interaction */
124
entry: PerformanceScriptTiming;
125
/** The INP subpart where the longest script ran */
126
subpart: 'input-delay' | 'processing-duration' | 'presentation-delay';
127
/** The amount of time the longest script intersected the INP duration */
128
intersectingDuration: number;
129
}
130
```
131
132
**Usage Example:**
133
134
```typescript
135
import { onINP } from "web-vitals/attribution";
136
137
onINP((metric) => {
138
console.log('INP time:', metric.value + 'ms');
139
console.log('Slow interaction target:', metric.attribution.interactionTarget);
140
console.log('Interaction type:', metric.attribution.interactionType);
141
142
// Timing breakdown
143
console.log('Input delay:', metric.attribution.inputDelay + 'ms');
144
console.log('Processing time:', metric.attribution.processingDuration + 'ms');
145
console.log('Presentation delay:', metric.attribution.presentationDelay + 'ms');
146
147
// Identify bottleneck
148
const bottleneck = Math.max(
149
metric.attribution.inputDelay,
150
metric.attribution.processingDuration,
151
metric.attribution.presentationDelay
152
);
153
154
if (bottleneck === metric.attribution.processingDuration) {
155
console.log('Bottleneck: JavaScript processing time');
156
} else if (bottleneck === metric.attribution.presentationDelay) {
157
console.log('Bottleneck: Rendering/paint time');
158
} else {
159
console.log('Bottleneck: Input delay');
160
}
161
});
162
```
163
164
### LCP Attribution
165
166
Provides detailed information about the largest contentful paint element and loading phases.
167
168
```typescript { .api }
169
/**
170
* LCP measurement with attribution data for debugging loading performance
171
* @param callback - Function to receive LCP metric with attribution data
172
* @param opts - Optional configuration including custom target generation
173
*/
174
function onLCP(callback: (metric: LCPMetricWithAttribution) => void, opts?: AttributionReportOpts): void;
175
176
interface LCPMetricWithAttribution extends LCPMetric {
177
attribution: LCPAttribution;
178
}
179
180
interface LCPAttribution {
181
/** CSS selector of the LCP element */
182
target?: string;
183
/** URL of the LCP resource (for images) */
184
url?: string;
185
/** Time from page load initiation to first byte received (TTFB) */
186
timeToFirstByte: number;
187
/** Delta between TTFB and when browser starts loading LCP resource */
188
resourceLoadDelay: number;
189
/** Total time to load the LCP resource itself */
190
resourceLoadDuration: number;
191
/** Delta from resource load finish to LCP element fully rendered */
192
elementRenderDelay: number;
193
/** Navigation entry for diagnosing general page load issues */
194
navigationEntry?: PerformanceNavigationTiming;
195
/** Resource entry for the LCP resource for diagnosing load issues */
196
lcpResourceEntry?: PerformanceResourceTiming;
197
}
198
```
199
200
**Usage Example:**
201
202
```typescript
203
import { onLCP } from "web-vitals/attribution";
204
205
onLCP((metric) => {
206
console.log('LCP time:', metric.value + 'ms');
207
console.log('LCP element:', metric.attribution.element);
208
console.log('LCP resource URL:', metric.attribution.url);
209
210
// Loading phase breakdown
211
console.log('Time to resource start:', metric.attribution.timeToFirstByte + 'ms');
212
console.log('Resource load delay:', metric.attribution.resourceLoadDelay + 'ms');
213
console.log('Resource load duration:', metric.attribution.resourceLoadDuration + 'ms');
214
console.log('Element render delay:', metric.attribution.elementRenderDelay + 'ms');
215
216
// Identify optimization opportunities
217
if (metric.attribution.resourceLoadDelay > 100) {
218
console.log('Consider preloading the LCP resource');
219
}
220
if (metric.attribution.elementRenderDelay > 50) {
221
console.log('Consider optimizing render-blocking resources');
222
}
223
});
224
```
225
226
### FCP Attribution
227
228
Provides information about the first contentful paint and potential blocking resources.
229
230
```typescript { .api }
231
/**
232
* FCP measurement with attribution data for debugging first paint
233
* @param callback - Function to receive FCP metric with attribution data
234
* @param opts - Optional configuration including custom target generation
235
*/
236
function onFCP(callback: (metric: FCPMetricWithAttribution) => void, opts?: AttributionReportOpts): void;
237
238
interface FCPMetricWithAttribution extends FCPMetric {
239
attribution: FCPAttribution;
240
}
241
242
interface FCPAttribution {
243
/** Time from page load initiation to first byte received (TTFB) */
244
timeToFirstByte: number;
245
/** Delta between TTFB and first contentful paint */
246
firstByteToFCP: number;
247
/** Loading state when FCP occurred */
248
loadState: LoadState;
249
/** PerformancePaintTiming entry corresponding to FCP */
250
fcpEntry?: PerformancePaintTiming;
251
/** Navigation entry for diagnosing general page load issues */
252
navigationEntry?: PerformanceNavigationTiming;
253
}
254
```
255
256
### TTFB Attribution
257
258
Provides detailed breakdown of server response time components.
259
260
```typescript { .api }
261
/**
262
* TTFB measurement with attribution data for debugging server response
263
* @param callback - Function to receive TTFB metric with attribution data
264
* @param opts - Optional configuration including custom target generation
265
*/
266
function onTTFB(callback: (metric: TTFBMetricWithAttribution) => void, opts?: AttributionReportOpts): void;
267
268
interface TTFBMetricWithAttribution extends TTFBMetric {
269
attribution: TTFBAttribution;
270
}
271
272
interface TTFBAttribution {
273
/** Total time from user initiation to page start handling request */
274
waitingDuration: number;
275
/** Total time spent checking HTTP cache for a match */
276
cacheDuration: number;
277
/** Total time to resolve DNS for the requested domain */
278
dnsDuration: number;
279
/** Total time to create connection to the requested domain */
280
connectionDuration: number;
281
/** Time from request sent to first byte received (includes network + server time) */
282
requestDuration: number;
283
/** Navigation entry for diagnosing general page load issues */
284
navigationEntry?: PerformanceNavigationTiming;
285
}
286
```
287
288
**Usage Example:**
289
290
```typescript
291
import { onTTFB } from "web-vitals/attribution";
292
293
onTTFB((metric) => {
294
console.log('TTFB time:', metric.value + 'ms');
295
296
// Network timing breakdown
297
console.log('DNS lookup:', metric.attribution.dnsDuration + 'ms');
298
console.log('Connection time:', metric.attribution.connectionDuration + 'ms');
299
console.log('Server response:', metric.attribution.requestDuration + 'ms');
300
301
// Optimization suggestions
302
if (metric.attribution.dnsDuration > 20) {
303
console.log('Consider DNS prefetching or using a faster DNS resolver');
304
}
305
if (metric.attribution.connectionDuration > 100) {
306
console.log('Consider using HTTP/2 or reducing connection setup time');
307
}
308
if (metric.attribution.requestDuration > 600) {
309
console.log('Consider server-side optimizations or CDN usage');
310
}
311
});
312
```
313
314
## Configuration Options
315
316
### Custom Target Generation
317
318
The attribution build allows customizing how DOM elements are converted to CSS selectors for debugging.
319
320
```typescript { .api }
321
interface AttributionReportOpts extends ReportOpts {
322
/**
323
* Custom function to generate target selectors for DOM elements
324
* @param el - The DOM element to generate a selector for
325
* @returns A string selector, null, or undefined (falls back to default)
326
*/
327
generateTarget?: (el: Node | null) => string | null | undefined;
328
}
329
```
330
331
**Usage Example:**
332
333
```typescript
334
import { onCLS } from "web-vitals/attribution";
335
336
onCLS((metric) => {
337
console.log('Custom element selector:', metric.attribution.largestShiftTarget);
338
}, {
339
generateTarget: (element) => {
340
// Custom selector generation logic
341
if (element?.id) {
342
return `#${element.id}`;
343
}
344
if (element?.className) {
345
return `.${element.className.split(' ')[0]}`;
346
}
347
return element?.tagName?.toLowerCase() || 'unknown';
348
}
349
});
350
```
351
352
## Supported Types
353
354
```typescript { .api }
355
type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete';
356
357
interface AttributionReportOpts extends ReportOpts {
358
generateTarget?: (el: Node | null) => string | null | undefined;
359
}
360
361
interface INPAttributionReportOpts extends AttributionReportOpts {
362
durationThreshold?: number;
363
}
364
```