perf: cut share page client JS by 45% (922KB → 508KB gz) - #2102
Merged
Conversation
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.
|
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. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
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: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 pureupload-progress.tspredicates plus a headlessUploadProgressTrackerthat only mounts while a video actually has a live upload. Initial "fetching" state and enable/disable flips mirror the old inline hook exactly.DuplicateCapMenuItem/DeleteCapDialog, loaded when the owner menu is used. Duplicate reads pending state viauseIsMutating, which also fixes a pre-existing edge where reopening the menu mid-flight could double-fire.actions/videos/translation-languages.tsre-exported a value through the@cap/web-domainbarrel into client code — now a deep import of the dependency-freeLanguage.ts.Toolbar/Commentsswitch 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).
UpgradeModalis latch-mounted behinddynamic()at all three sites. The dashboard context objects moved toDashboardContext.ts(withContexts.tsxre-exporting) so readinguseDashboardContextoutside the dashboard no longer dragsInviteDialog/UpgradeModalalong.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), andSummaryChapters(react-markdown — only fetched when AI data exists).Render scheduling.
Share.tsxmemoizeseffectiveDateand thedataspreads handed toShareVideo/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 --noEmitclean; full vitest suite: 1,459 passed (only failure is the pre-existing slack-manifest color drift on main).from-now.test.tsparity suite passes.no-momentresolved.Notes for reviewers
Greptile Summary
This PR substantially reduces the share page's initial client bundle without intended functional changes.
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
Reviews (1): Last reviewed commit: "perf(web): stop the playback rAF loop wh..." | Re-trigger Greptile
Context used: