Skip to content

Commit 908e4ec

Browse files
Lms24timfish
authored andcommitted
fix(browser): Ignore React 19.2+ component render measure entries (#17905)
With 19.2, React introduced [custom perfomance tracks](https://react.dev/blog/2025/10/01/react-19-2#performance-tracks) in chrome dev tools. This track is populated by collecting `performance.measure` entries for every component (re-)render. Sounds good in theory but in reality this causes a massive performance degradation when using the Sentry SDK because we collect spans from `PerformanceMeasure` entries. In our Sentry UI, this caused 10+ second long blocks because we created thousands of spans from these render entries. This patch fixes this performance drop by inspecting the measure entries' `detail` object which we can use to _fairly well_ distinguish React's entries from users' entries. Not 100% bulletproof but I think good enough.
1 parent 4308b37 commit 908e4ec

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

packages/browser-utils/src/metrics/browserMetrics.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,24 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
425425
_measurements = {};
426426
}
427427

428+
/**
429+
* React 19.2+ creates performance.measure entries for component renders.
430+
* We can identify them by the `detail.devtools.track` property being set to 'Components ⚛'.
431+
* see: https://react.dev/reference/dev-tools/react-performance-tracks
432+
* see: https://github.com/facebook/react/blob/06fcc8f380c6a905c7bc18d94453f623cf8cbc81/packages/react-reconciler/src/ReactFiberPerformanceTrack.js#L454-L473
433+
*/
434+
function isReact19MeasureEntry(entry: PerformanceEntry | null): boolean | void {
435+
if (entry?.entryType !== 'measure') {
436+
return;
437+
}
438+
try {
439+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
440+
return (entry as PerformanceMeasure).detail.devtools.track === 'Components ⚛';
441+
} catch {
442+
return;
443+
}
444+
}
445+
428446
/**
429447
* Create measure related spans.
430448
* Exported only for tests.
@@ -437,6 +455,10 @@ export function _addMeasureSpans(
437455
timeOrigin: number,
438456
ignorePerformanceApiSpans: AddPerformanceEntriesOptions['ignorePerformanceApiSpans'],
439457
): void {
458+
if (isReact19MeasureEntry(entry)) {
459+
return;
460+
}
461+
440462
if (
441463
['mark', 'measure'].includes(entry.entryType) &&
442464
stringMatchesSomePattern(entry.name, ignorePerformanceApiSpans)
@@ -445,6 +467,7 @@ export function _addMeasureSpans(
445467
}
446468

447469
const navEntry = getNavigationEntry(false);
470+
448471
const requestTime = msToSec(navEntry ? navEntry.requestStart : 0);
449472
// Because performance.measure accepts arbitrary timestamps it can produce
450473
// spans that happen before the browser even makes a request for the page.

packages/browser-utils/test/browser/browserMetrics.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,75 @@ describe('_addMeasureSpans', () => {
186186
]),
187187
);
188188
});
189+
190+
it('ignores React 19.2+ measure spans', () => {
191+
const pageloadSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true });
192+
const spans: Span[] = [];
193+
194+
getClient()?.on('spanEnd', span => {
195+
spans.push(span);
196+
});
197+
198+
const entries: PerformanceMeasure[] = [
199+
{
200+
entryType: 'measure',
201+
name: '\u200bLayout',
202+
duration: 0.3,
203+
startTime: 12,
204+
detail: {
205+
devtools: {
206+
track: 'Components ⚛',
207+
},
208+
},
209+
toJSON: () => ({ foo: 'bar' }),
210+
},
211+
{
212+
entryType: 'measure',
213+
name: '\u200bButton',
214+
duration: 0.1,
215+
startTime: 13,
216+
detail: {
217+
devtools: {
218+
track: 'Components ⚛',
219+
},
220+
},
221+
toJSON: () => ({}),
222+
},
223+
{
224+
entryType: 'measure',
225+
name: 'Unmount',
226+
duration: 0.1,
227+
startTime: 14,
228+
detail: {
229+
devtools: {
230+
track: 'Components ⚛',
231+
},
232+
},
233+
toJSON: () => ({}),
234+
},
235+
{
236+
entryType: 'measure',
237+
name: 'my-measurement',
238+
duration: 0,
239+
startTime: 12,
240+
detail: null,
241+
toJSON: () => ({}),
242+
},
243+
];
244+
245+
const timeOrigin = 100;
246+
const startTime = 23;
247+
const duration = 356;
248+
249+
entries.forEach(e => {
250+
_addMeasureSpans(pageloadSpan, e, startTime, duration, timeOrigin, []);
251+
});
252+
253+
expect(spans).toHaveLength(1);
254+
expect(spans.map(spanToJSON)).toEqual(
255+
expect.arrayContaining([expect.objectContaining({ description: 'my-measurement', op: 'measure' })]),
256+
);
257+
});
189258
});
190259

191260
describe('_addResourceSpans', () => {

0 commit comments

Comments
 (0)