From 6c65ae82575218ad52148926025bba29cb77131c Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 9 Jun 2026 22:05:04 +0200 Subject: [PATCH] perf(twenty-front): stop Sentry Replay from re-serializing record-table mutations on navigation (#21381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Navigating between record-index pages (e.g. People ↔ Companies) blocks the main thread for seconds, on every navigation, for ~every user. Profiling pointed at **Sentry Session Replay(rrweb)**, not app code. ## Root cause Swapping one record table for another produces a large DOM mutation batch. rrweb serializes that batch **synchronously on the main thread** (`_isParentRemoved` / mutation processing). The built-in `mutationLimit` safety valve doesn't help: it's a *count* threshold (default 10000), but our cost is *per-mutation serialization* on a wide/deep table DOM — the batch is expensive, not numerous, so it slips under the limit. ## Fix ```ts replayIntegration({ _experiments: { ignoreMutations: ['[id^="row-virtual-index-"]'], }, }), ``` - ignoreMutations tells rrweb to drop mutation batches originating from the virtualized row containers (StyledVirtualizedRowContainer, ids row-virtual-index-N) — the source of the table-swap churn. The table still appears in replays (initial snapshot; text is already masked by default), its live row updates just aren't re-serialized. ## Test Measured locally ``` ┌────────────────────────────────────────────────────────────┬───────────┬───────────────┐ │ │ Baseline │ With fix │ ├────────────────────────────────────────────────────────────┼───────────┼───────────────┤ │ Replay/rrweb total │ 4,112 ms │ 188 ms (−95%) │ ├────────────────────────────────────────────────────────────┼───────────┼───────────────┤ │ _isParentRemoved │ 2,195 ms │ 6 ms │ ├────────────────────────────────────────────────────────────┼───────────┼───────────────┤ ``` ## Tradeoff ignoreMutations tells rrweb to skip mutation batches coming from the virtualized record-table rows, so session replays won't reflect live changes inside the table — rows scrolling, cells updating, inline edits will appear "frozen" at the last full snapshot. The table still shows in the replay (initial render), and **its text is masked by default anyway, so in practice we lose little**: the surrounding UI, navigation, clicks, and interactions are all still recorded. The cost we're removing (multi-second main-thread freeze on every navigation, for ~all users) **far outweighs not seeing table row churn in replays** imho. (@FelixMalfait @charlesBochet) Two caveats worth noting: _experiments.ignoreMutations is an experimental Sentry API, and it's batch-coarse, if a mutation batch contains any matching element, the whole batch is dropped, so an unrelated change occasionally batched with table mutations could be missed. During navigation these batches are almost entirely table mutations, so collateral is minimal. If it ever proves insufficient, the reliable fallback is `data-sentry-block` on the record-table body (which turns the table into a placeholder box in replays). --------- Co-authored-by: Cursor Agent Co-authored-by: Félix Malfait --- .../ai/components/AiChatAssistantMessageRenderer.tsx | 2 +- .../src/modules/ai/components/LazyMarkdownRenderer.tsx | 5 ++++- .../error-handler/components/SentryInitEffect.tsx | 9 ++++++++- .../constants/SentryReplayIgnoreMutationsAttribute.ts | 2 ++ .../components/RecordBoardColumnCardsContainer.tsx | 2 +- .../components/RecordTableBodyRecordGroupDroppable.tsx | 1 + .../components/RecordTableRowVirtualizedContainer.tsx | 1 + 7 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-front/src/modules/error-handler/constants/SentryReplayIgnoreMutationsAttribute.ts diff --git a/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx index 4846e846ef..b2e0d3803d 100644 --- a/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AiChatAssistantMessageRenderer.tsx @@ -115,7 +115,7 @@ export const AiChatAssistantMessageRenderer = ({ return (
- + {renderItems.map((renderItem, index) => renderItem.type === 'thinking-steps' ? ( { export const LazyMarkdownRenderer = ({ text }: { text: string }) => { return ( - + }> { dsn: sentryConfig?.dsn, integrations: [ browserTracingIntegration({}), - replayIntegration(), + replayIntegration({ + _experiments: { + ignoreMutations: [ + `[${SENTRY_REPLAY_IGNORE_MUTATIONS_ATTRIBUTE}]`, + ], + }, + }), globalHandlersIntegration({ onunhandledrejection: false, // handled in PromiseRejectionEffect }), diff --git a/packages/twenty-front/src/modules/error-handler/constants/SentryReplayIgnoreMutationsAttribute.ts b/packages/twenty-front/src/modules/error-handler/constants/SentryReplayIgnoreMutationsAttribute.ts new file mode 100644 index 0000000000..ef43e8986e --- /dev/null +++ b/packages/twenty-front/src/modules/error-handler/constants/SentryReplayIgnoreMutationsAttribute.ts @@ -0,0 +1,2 @@ +export const SENTRY_REPLAY_IGNORE_MUTATIONS_ATTRIBUTE = + 'data-replay-ignore-mutations'; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index 0c86513a92..fb646e860e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -43,7 +43,7 @@ export const RecordBoardColumnCardsContainer = ({ ); return ( - + {recordIndexRecordIdsByGroup.map((recordId, index) => ( {TABLE_VIRTUALIZATION_DEBUG_ACTIVATED && (