Skip to content

perf: cut share page client JS by 45% (922KB → 508KB gz) - #2102

Merged
richiemcilroy merged 3 commits into
mainfrom
perf/share-page-bundle
Aug 7, 2026
Merged

perf: cut share page client JS by 45% (922KB → 508KB gz)#2102
richiemcilroy merged 3 commits into
mainfrom
perf/share-page-bundle

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Aug 7, 2026

Copy link
Copy Markdown
Member

Performance pass on the share page (/s/[videoId]). No functional or visual changes intended — every change is client-bundle surgery or render scheduling; SSR paths, APIs and the database are untouched.

Results

Measured on a production build (next start), 7-run medians against a real 4K webMP4 recording, anonymous viewer:

Metric Before After
JS transferred 922 KB gz (29 scripts) 508 KB gz (25 scripts)
JS decoded 3.19 MB 1.69 MB
Total blocking time (4x CPU throttle) 253 ms 29 ms
Video ready to play ~3.2 s ~2.8 s
JS heap after load 22 MB 12 MB
TTFB / FCP 31 ms / 72 ms unchanged

What changed

Effect runtime out of the entry chunk (−246 KB gz). It leaked in through three doors:

  • useUploadProgress (RPC client) statically imported by all three players — split into pure upload-progress.ts predicates plus a headless UploadProgressTracker that only mounts while a video actually has a live upload. Initial "fetching" state and enable/disable flips mirror the old inline hook exactly.
  • ShareHeader's duplicate/delete mutations — extracted into DuplicateCapMenuItem / DeleteCapDialog, loaded when the owner menu is used. Duplicate reads pending state via useIsMutating, which also fixes a pre-existing edge where reopening the menu mid-flight could double-fire.
  • actions/videos/translation-languages.ts re-exported a value through the @cap/web-domain barrel into client code — now a deep import of the dependency-free Language.ts. Toolbar/Comments switch to type-only imports with casts for the constraint-free branded ids.

moment gone (−75 KB gz). The header's single .fromNow() call now uses a local util that replicates moment's humanize algorithm exactly (calendar-aware month diffing included). A new unit test pins parity using moment itself as the oracle across every threshold boundary and a four-year sweep.

Rive out of initial load (~−50 KB gz). UpgradeModal is latch-mounted behind dynamic() at all three sites. The dashboard context objects moved to DashboardContext.ts (with Contexts.tsx re-exporting) so reading useDashboardContext outside the dashboard no longer drags InviteDialog/UpgradeModal along.

More lazy seams, all following the existing ShareLinkDialog latch-and-prewarm pattern: Sharing/Settings/Password dialogs, sidebar Summary/Transcript/Settings tabs (hover on a tab prewarms its chunk), CommentMiniPlayer (keeps media-chrome out of the entry chunk), AuthOverlay (next-auth), and SummaryChapters (react-markdown — only fetched when AI data exists).

Render scheduling. Share.tsx memoizes effectiveDate and the data spreads handed to ShareVideo/Sidebar, so the 2-second status poll while a video processes no longer re-renders both subtrees with fresh object identities. The playback store's 60 fps rAF loop now only runs while frame-rate subscribers exist (classic view has none; timeline view is unchanged).

Verification

  • tsc --noEmit clean; full vitest suite: 1,459 passed (only failure is the pre-existing slack-manifest color drift on main).
  • New from-now.test.ts parity suite passes.
  • Browser-automation pass on the production build: video plays, captions render, relative date correct, lazy Transcript/Summary tabs load with content, Timeline view mounts and returns, share dialog opens, no page errors.
  • Post-change lint scan confirms no new findings introduced; no-moment resolved.

Notes for reviewers

  • The one path verified by reasoning rather than observation is a share page whose video is still uploading (needs a live desktopSegments upload, which the local rig couldn't produce). The tracker preserves the old hook's state machine — including the "fetching" initial state that gates playback deferral — but it's worth a staging check: record a video and open its share page mid-upload.
  • HLS pages verified to SSR and mount the player; actual segment streaming couldn't complete locally for environment reasons (media not materialized locally). The hls.js load path itself is untouched.

Greptile Summary

This PR substantially reduces the share page's initial client bundle without intended functional changes.

  • Splits Effect runtime, Moment, Rive, authentication, media-player, dialog, and non-default-tab dependencies from the initial share-page bundle.
  • Preserves upload-progress behavior through dynamically mounted trackers and dependency-free state helpers.
  • Memoizes frequently propagated share data and gates playback frame scheduling on active subscribers.
  • Adds Moment-parity coverage for the local relative-time formatter.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code regression identified.

The upload-progress state bridge preserves the prior defer and completion transitions, subscriber-gated playback scheduling has correct stop and restart guards, and the lazy UI conditions retain the prior rendered states.

Important Files Changed

Filename Overview
apps/web/app/s/[videoId]/Share.tsx Adds stable derived data objects and lazily renders summary content only under the component's existing meaningful-data conditions.
apps/web/app/s/[videoId]/_components/ShareHeader.tsx Moves interaction-only dialogs and mutations behind dynamic latch-mounted components and replaces Moment with a local formatter.
apps/web/app/s/[videoId]/_components/ShareVideo.tsx Lazily mounts upload tracking and the upgrade modal while preserving the existing upload-state transitions.
apps/web/app/s/[videoId]/_components/UploadProgressTracker.tsx Bridges the existing upload-progress query into player-local state only while an upload is active.
apps/web/app/s/[videoId]/_components/playback/playback-store.ts Stops frame-rate sampling without subscribers and restarts it when a subscriber joins active playback.
apps/web/app/s/[videoId]/_components/utils/from-now.ts Implements the English Moment relative-time behavior with calendar-aware month calculations and dedicated parity tests.
apps/web/app/s/[videoId]/_components/Sidebar.tsx Dynamically loads non-default sidebar tabs and prewarms the largest tabs on pointer or keyboard focus.
apps/web/app/(org)/dashboard/DashboardContext.ts Separates lightweight dashboard context definitions from provider-only dialog dependencies without changing the context contract.

Reviews (1): Last reviewed commit: "perf(web): stop the playback rAF loop wh..." | Re-trigger Greptile

Context used:

New modules that let the share page reference upload-progress state,
relative timestamps, owner actions and the auth overlay without pulling
their heavy dependencies into the entry chunk:

- upload-progress.ts: pure predicates + UploadProgress type split out of
  ProgressCircle, no Effect imports
- UploadProgressTracker.tsx: headless bridge around useUploadProgress so
  the Effect-backed RPC polling only mounts while an upload is live
- utils/from-now.ts: exact reimplementation of moment(date).fromNow()
  (calendar-month diff + moment's humanize thresholds), pinned by a
  parity test that sweeps four years of offsets against moment itself
- AuthOverlayLazy.tsx: latch-mounted wrapper that keeps next-auth and
  the OTP form off the wire until a signed-out viewer hits an auth wall
- DeleteCapDialog.tsx / DuplicateCapMenuItem.tsx: owner actions with
  their RPC mutations, loaded when the owner menu is actually used;
  duplicate reads pending state via useIsMutating so a remounted menu
  cannot double-fire an in-flight duplication
- DashboardContext.ts: context objects + hooks split from Contexts.tsx
  so non-dashboard consumers stop dragging the provider's dialogs
Anonymous-viewer JS for /s/[videoId] drops 45% (29 -> 25 scripts,
decoded 3.2MB -> 1.7MB) by severing the three biggest dependency
subtrees from the entry chunk and lazy-loading interaction-only UI:

- Effect runtime (246KB gz): the players now hold plain upload-progress
  state fed by the lazily-mounted tracker; ShareHeader's duplicate and
  delete moved to the extracted lazy components; translation-languages
  re-exports from @cap/web-domain/src/Language instead of the package
  barrel; Toolbar/Comments use type-only web-domain imports with casts
  for the constraint-free branded ids
- moment + locales (75KB gz): the header's single fromNow() call now
  uses the local moment-parity util
- Rive via UpgradeModal (~50KB gz): latch-mounted behind dynamic() at
  every site (ShareVideo watermark, ShareHeader, dashboard Contexts),
  and ShareHeader reads useDashboardContext from the split context
  module so the provider's dialogs stay out of its graph
- Sharing/Settings/Password dialogs follow the existing ShareLinkDialog
  latch pattern; sidebar Summary/Transcript/Settings tabs code-split
  with hover prewarm; CommentMiniPlayer (media-chrome) and AuthOverlay
  (next-auth) load on first use; SummaryChapters (react-markdown) only
  renders - and only fetches - once AI data exists

Share.tsx also memoizes effectiveDate and the data spreads handed to
ShareVideo/Sidebar, so the 2s status poll during processing no longer
re-renders both subtrees with fresh object identities.

Total blocking time at 4x CPU throttle drops 253ms -> 29ms; JS heap
after load 22MB -> 12MB. TTFB/FCP unchanged (SSR paths untouched).
…e rate

The store's 60fps sampling loop ran for the whole playback session even
in classic view, where no timeline consumers are mounted and the only
listeners are the coarse event-driven samples. The loop now starts only
while timeSubscribers exist (and starts on subscribe if playback is
already running), and stops itself when the last subscriber leaves.
Timeline view behavior is unchanged - its subscribers still get the
frame-rate loop the moment they mount.
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@richiemcilroy
richiemcilroy merged commit 43f8115 into main Aug 7, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant