Skip to content

[RUM] /sign-in blocks the main thread 8.5× harder than the rest of Studio — eager marketing tags, p95 1,136ms across 335 sessions #1684

Description

@dawsontoth

/sign-in is Studio's main-thread hotspot, and almost all of the blocking is third-party
marketing tags that src/App.tsx loads eagerly above the router. This is a standing
condition, not a spike
— which is why the usual adjacent-window comparison never surfaced
it. p95 and p99 are flat across 14d and 30d.

The measurement

@long_task.blocking_duration p95 (LoAF blocking time — the actual main-thread cost, not
frame duration), production RUM app f590deee, 30d to 2026-09-07:

Scope frames p95 blocking
@view.url_hash:*sign-in* 5,572 1,136ms
everything else 22,964 134ms

An 8.5× gap. Stable over time — sign-in p95 was 1,182ms on 14d, 1,136ms on 30d.

Sign-in is one screen out of ~40, and it accounts for 19.5% of all long animation frames
in the application (5,572 of 28,536).

Reach, 30d:

  • 335 sessions emitted at least one long animation frame on sign-in, out of 1,322 sessions
    that viewed sign-in at all (25%).
  • 314 frames blocked the main thread for over 1 second, across 84 sessions (6.4% of
    sign-in sessions).
  • Worst single frame: 5,445ms duration / 5,392ms blocking.

That last ratio matters. blocking_duration ≈ duration on the worst frames, so this is real
main-thread work — not the rAF-starvation pattern (render_start - start_time ≈ duration
with an empty scripts array) that made a previous long-task investigation a false positive.
@long_task.entry_type is long-animation-frame for 28,303 of 28,536 frames over the same
30d, so the collection mode has not changed either.

Attribution

Taking only frames where LoAF attributed exactly one distinct script host — so the
attribution is unambiguous — over 14d of sign-in frames:

Attributed host frames total blocking avg/frame
static.reo.dev (Reo.dev intent pixel) 90 26,619ms 296ms
www.googletagmanager.com 41 12,295ms 300ms
fabric.harper.fast (our own bundle) 44 9,702ms 220ms
connect.facebook.net 21 4,949ms 236ms
js.stripe.com 11 2,796ms 254ms
www.redditstatic.com 6 430ms 72ms
capi-automation.s3.us-east-2.amazonaws.com 4 328ms 82ms
snap.licdn.com (LinkedIn Insight) 3 173ms 58ms
js-na2.hs-analytics.net (HubSpot) 9 115ms 13ms

Third-party tags are ~48s of the ~58s of unambiguously-attributed blocking. Our own bundle is
9.7s of it — worth its own look, but it is not the bulk.

The most common individual invokers are TimerHandler:setTimeout | static.reo.dev (118),
static.reo.dev bare (43), SCRIPT[src="static.reo.dev…"] (29),
IdleRequestCallback | static.reo.dev (26), Permissions.query.then | static.reo.dev (17),
and TimerHandler:setTimeout | www.googletagmanager.com (39).

Mechanism

src/App.tsx calls all three integrations unconditionally, above
the router, on first render:

export function App() {
	useReo();
	useDatadog();
	useGTM();
  • useReo (src/integrations/reo/reo.ts) injects static.reo.dev from a useEffect with no
    deferral.
  • useGTM (src/integrations/google/gtm.ts) injects container GTM-5QQX432, which in turn
    loads Facebook, LinkedIn, HubSpot (analytics + collected-forms + banner), Reddit, Apollo,
    and aplo-evnt.com. None of those hosts appear anywhere in this repo — they arrive through
    the container, so the blast radius of the GTM tag is whatever marketing has configured.

Because these are above the router, they load on every entry point. Sign-in absorbs the
whole cost because sign-in is where an unauthenticated visitor's initial load happens — all
seven-plus tag bundles boot concurrently with app hydration. Once past the auth wall, route
changes are cheap because the scripts are already parsed, which is exactly the 134ms figure
in the table above.

Note also #1620: ~90% of Reo's own event POSTs have been rejected 400 for 30+ days. So the
single largest blocking contributor on the sign-in critical path is currently paying full
main-thread cost for telemetry that mostly does not land.

Honest caveats

Suggested direction

Ordered cheapest-first; none of these removes a tag:

  1. Get the tags off the initial-load critical path. Defer useReo() and useGTM() behind
    requestIdleCallback (or a setTimeout after first paint) instead of firing them in a
    mount effect. Preserves every tag and every pageview; only the timing moves.
  2. Reconsider loading Reo on the auth screens at all, given [RUM] Reo.dev intent pixel: 90% of event POSTs rejected 400 for 30+ days, on 66% of sessions #1620. It is the largest single
    contributor and its payloads are being rejected.
  3. Separately, look at the 9.7s attributed to our own bundle on sign-in — smaller, but it is
    ours to fix outright.

(1) is a measurement-vs-performance tradeoff that marketing owns, not purely an engineering
call — deferring a tag can shift attribution for the acquisition funnel — so this is filed for
that conversation rather than fixed unilaterally.

Reproducing the numbers

APP='@application.id:f590deee-4bac-49b4-a202-3b6963d9721d'
pup rum aggregate --query "$APP @type:long_task @view.url_hash:*sign-in*" \
  --compute 'percentile(@long_task.blocking_duration, 95)' --from 30d --to now
pup rum aggregate --query "$APP @type:long_task -@view.url_hash:*sign-in*" \
  --compute 'percentile(@long_task.blocking_duration, 95)' --from 30d --to now
pup rum events    --query "$APP @type:long_task @view.url_hash:*sign-in*" \
  --from 14d --to now --limit 600    # then group scripts[].source_url by host

Values come back in nanoseconds. Found by the daily RUM review, 2026-09-07.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    rumFrom real user monitoring where we aim to keep users happy

    Type

    Fields

    Priority

    P2

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions