Skip to content

Session recording (rrweb) blocks the main thread for several seconds serializing a large DOM #4217

Description

@kelp-ganesh

Bug description

When session recording is enabled, posthog-recorder.js (rrweb) serializes a large DOM mutation batch / full snapshot synchronously on the main thread, freezing the tab for several seconds. It happens when a large DOM subtree (a data-dense table, ~10k DOM nodes) is rebuilt or re-snapshotted on navigation — the kind of DOM common in data-heavy UIs.

A Chrome Performance profile of the affected interaction shows the recorder dominating the main thread:

  • Grouped by 1st/3rd party: posthog.com consumed 22,444.6 ms of main-thread time over a 29.6 s capture (~76% of the profile) while transferring only 0.6 kB — i.e. pure CPU, not network.
  • Bottom-up (function level): effectively all heavy self-time resolves to rrweb-record.js, dominated by synchronous DOM traversal during serialization.
  • The Frames track shows multi-second frozen frames during the interaction.

Disabling session recording removes the freeze entirely, confirming the recorder is the cause.

We also saw this begin without any change to our application build. The recorder is fetched at runtime from https://us-assets.i.posthog.com/static/posthog-recorder.js, and the ?v= query is cache-busting only (not version pinning), so an unchanged SDK build can start receiving a newer recorder. The served asset's last-modified was recent, which lines up with when the slowdown appeared.

How to reproduce

  1. Enable Session Replay for a project.
  2. Render a page with a large DOM — e.g. a data table with many node-dense rows (avatars, tag chips, action buttons). Roughly ~50 rows × ~190 nodes/row ≈ ~10k nodes, with realistic per-node attributes/inline styles.
  3. Trigger a re-render that rebuilds the table (switch columns/dataset), or navigate so rrweb takes a full re-snapshot.
  4. Observe a multi-second main-thread freeze; a Performance profile attributes the time to rrweb-record.js (DOM serialization / parentNode traversal).

Setup

PostHog is initialized once at app start; session recording uses PostHog defaults.

import posthog from "posthog-js";

posthog.init(POSTHOG_PROJECT_KEY, {
  api_host: POSTHOG_HOST, // PostHog Cloud (US)
  loaded: (ph) => ph.identify(userEmail),
});
// No `session_recording` overrides; session replay enabled via project settings.

Related sub-libraries

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin

Additional context

  • Reproduced on the latest posthog-js (1.405.2), and on earlier releases — not specific to a single version.
  • attributeFilter reduces attribute-mutation churn but not the structural add/remove batch or the full snapshot.
  • This isn't specific to one app's markup: the DOM itself didn't change — the same DOM recorded acceptably before the recorder asset changed, and Session recorder blocks main thread #3273 reports the same freeze in an unrelated app. Large DOMs are normal in data-heavy UIs; the recorder should serialize them without blocking the main thread.
  • Screenshots attached: (1) Performance → Summary, 1st/3rd-party grouping; (2) Performance → Bottom-up.
Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions