You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Repeatedly moving a large subtree makes session recording walk its descendants many times. Each walk also creates per-node callbacks for NodeList.forEach, adding main-thread overhead.
This PR is stacked on #4808. The baseline already includes the getter-cache, serialization-options and mirror-removal improvements. This is a partial improvement for #4217, not a complete fix.
Changes
Replace child-list forEach calls in genAdds and deepDelete with indexed loops. Preserve the initial list length, read each child from the live list and skip slots removed during recursion.
Keep light/shadow traversal, moved-set ordering and right-to-left depth-first deletion order unchanged. No records or subtree visits are skipped, and recording stays synchronous.
Add six focused regression tests. The repeated-move test failed before the change with 4,422 forEach calls instead of 201. The remaining 201 calls belong to the unchanged processRemoves traversal.
Extend the opt-in benchmark with deep trees, mixed child/attribute/masking changes and a separate diagnostic build for visit and set-operation counters. Diagnostic artifacts cannot be used in unprofiled timing runs.
Performance
Three alternating baseline/candidate runs per shape, approximately 50k nodes, compression enabled, Apple M4 Pro and Chromium 136.0.7103.25. Both builds used the same harness source without profiling or counter instrumentation. No builds or tests ran concurrently with timings.
Shape, 11 moves in one batch
Median longest task before / after
Median input delay before / after
Table
475 / 375 ms
481.2 / 380.4 ms
Flat
485 / 378 ms
493.4 / 385.7 ms
Deep, 32 wrappers
483 / 395 ms
489.2 / 401.1 ms
Shadow
374 / 314 ms
382.0 / 319.5 ms
Mixed-move workloads also improved. Startup and child removal were effectively flat. These are descriptive desktop samples from a deliberate stress case, not statistical significance or typical customer impact. The candidate still blocks for roughly 300–400 ms.
Separate diagnostics confirmed identical node visits and set-operation counts before/after at 1, 3, 11 and 21 moves across all four shapes. Recorder size increased by 115 raw bytes and 80 gzip bytes.
327 recording/accessor tests passed, 2 skipped, including the six new tests and existing iframe/shadow/lifecycle coverage.
Nine masking tests passed across Chromium, Firefox and WebKit.
Small table/flat/deep/shadow/CSS preprocessing and ordering fixtures passed with compression on/off. Existing churn and legacy workloads also passed with both settings.
10k-node table/flat/deep fixtures passed with 4x page-only CPU throttling. Depth-40, one-move fixtures passed with both compression settings.
Emptying the movement data correctly failed the first repeated-move input checkpoint. Accepted benchmark runs reject privacy leaks, duplicate full-snapshot IDs, dropped mutations and unexpected recovery snapshots.
Depth 128 failed startup replay validation in both builds because rrweb already has a default snapshot depth limit of 50. This remains an explicitly excluded failure. No depth limit or fidelity check was relaxed.
SDK/dependency and rrweb builds/typechecks, targeted lint/format, syntax and ES5/ES6 checks passed. The code formatter hook was skipped to preserve existing rrweb formatting, without changing hook configuration.
Isolated autoreview passed on 9e3a6f22553b55bda60422e0631bc672d2be9d99 with no actionable findings, using autoreview --mode branch --base origin/perf/replay-mirror-ordering.
The incident checker flagged serializer correctness. Style serialization is unchanged, and real-browser checks verify intermediate ordering, masking, CSS and shadow state. There are no changes to lazy-load contracts, persisted fields, sampling, session rotation or flush policies. No recording-volume policy change is intended.
Release info Sub-libraries affected
Libraries affected
All of them
posthog-js (web)
posthog-js-lite (web lite)
posthog-node
posthog-react-native
@posthog/react-native-plugin
@posthog/react
@posthog/ai
@posthog/convex
@posthog/next
@posthog/nextjs-config
@posthog/nuxt
@posthog/openfeature-node-provider
@posthog/openfeature-web-provider
@posthog/rollup-plugin
@posthog/webpack-plugin
@posthog/types
@posthog/browser-common
Checklist
Tests for new code
Accounted for the impact of any changes across different platforms
Accounted for backwards compatibility of any changes (no breaking changes!)
Took care not to unnecessarily increase the bundle size
If releasing new changes
Added a posthog-js patch changeset
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Implemented with Pi using repository inspection, Chromium profiling, Vitest, Playwright and isolated autoreview. The change only replaces child enumeration. Traversal deduplication was not attempted because intervening removals and parent bookkeeping have different semantics from mirror cleanup.
Diagnostics remain separate from production timing comparisons. No asynchronous recording architecture was introduced. Human review is required.
This diff touches code involved in past incidents. This is a heads-up, not a verdict: read the matched sections of INCIDENTS.md and answer their review questions before merging.
For a judgment on whether this diff has the same failure mode, run the replay-incident-risk skill locally: open a Claude session in the repo root and ask it to review your diff with that skill. Or run node .agents/skills/replay-incident-risk/check.mjs and answer the review questions for each matched class yourself.
Matched 1 past incident pattern(s) (diff vs origin/perf/replay-mirror-ordering):
## rrweb serialization and snapshot correctness [path match]
Serializer bugs corrupt replays silently: no exception, no volume change. In May 2026 a cssText spec quirk silently dropped layout styles for sites using CSS custom properties. Test against framework-generated CSS (Chakra/Panda, Emotion speedy, Tailwind) in a real browser.
Touched paths:
- packages/rrweb/rrweb/src/record/mutation.ts
- packages/rrweb/rrweb/test/record/mutation-child-traversal.test.ts
Read: .agents/skills/replay-incident-risk/INCIDENTS.md#class-4-rrweb-serialization-and-snapshot-correctness
This check is advisory. It flags resemblance to past incidents, not correctness.
For a judgment pass on whether this diff has the same failure mode, run the
`replay-incident-risk` skill in a Claude session from the repo root, or answer the
review questions in the matched sections of .agents/skills/replay-incident-risk/INCIDENTS.md yourself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Repeatedly moving a large subtree makes session recording walk its descendants many times. Each walk also creates per-node callbacks for
NodeList.forEach, adding main-thread overhead.This PR is stacked on #4808. The baseline already includes the getter-cache, serialization-options and mirror-removal improvements. This is a partial improvement for #4217, not a complete fix.
Changes
forEachcalls ingenAddsanddeepDeletewith indexed loops. Preserve the initial list length, read each child from the live list and skip slots removed during recursion.forEachcalls instead of 201. The remaining 201 calls belong to the unchangedprocessRemovestraversal.Performance
Three alternating baseline/candidate runs per shape, approximately 50k nodes, compression enabled, Apple M4 Pro and Chromium 136.0.7103.25. Both builds used the same harness source without profiling or counter instrumentation. No builds or tests ran concurrently with timings.
Mixed-move workloads also improved. Startup and child removal were effectively flat. These are descriptive desktop samples from a deliberate stress case, not statistical significance or typical customer impact. The candidate still blocks for roughly 300–400 ms.
Separate diagnostics confirmed identical node visits and set-operation counts before/after at 1, 3, 11 and 21 moves across all four shapes. Recorder size increased by 115 raw bytes and 80 gzip bytes.
Investigation, reproduction commands and full results.
Validation and risk
9e3a6f22553b55bda60422e0631bc672d2be9d99with no actionable findings, usingautoreview --mode branch --base origin/perf/replay-mirror-ordering.The incident checker flagged serializer correctness. Style serialization is unchanged, and real-browser checks verify intermediate ordering, masking, CSS and shadow state. There are no changes to lazy-load contracts, persisted fields, sampling, session rotation or flush policies. No recording-volume policy change is intended.
Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
posthog-jspatch changeset🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Implemented with Pi using repository inspection, Chromium profiling, Vitest, Playwright and isolated autoreview. The change only replaces child enumeration. Traversal deduplication was not attempted because intervening removals and parent bookkeeping have different semantics from mirror cleanup.
Diagnostics remain separate from production timing comparisons. No asynchronous recording architecture was introduced. Human review is required.