Repository navigation
Description
The playground's Share button lz-compresses all of the selected instrument's files every time it renders, which blocks the main thread for about 200 ms per render when the instrument contains media. useState(encodeShareURL(...)) evaluates its argument on every render, not just the first, and throws the result away after mount. ShareButton re-renders when its tooltip opens and when it closes (hover in, hover out), when the popover opens, and whenever the selected instrument's label changes. Each of those renders compresses every file, including base64 media, on the main thread. For Interactive With Embedded Media (962 KB of file JSON), one compressToEncodedURIComponent call takes about 190 ms in Node. Hovering over the button costs two, opening the popover two more (the render plus the effect), and selecting the instrument one.
Where
apps/playground/src/components/Header/ShareButton/ShareButton.tsx:17-19:
const [shareURL, setShareURL] = useState(
encodeShareURL({ baseURL: window.location.origin, files: editorFilesRef.current, label })
);Reproduce
Mechanism and scale: select Interactive With Embedded Media, start a Performance recording, and move the pointer over and off the Share button. Each pass records two long tasks in compressToEncodedURIComponent. The cost grows linearly with the instrument's total file size. Interactive With Static Assets (354 KB) takes about 70 ms per render.
Tests
apps/playground/src/components/Header/ShareButton/__tests__/ShareButton.test.tsx (happy-dom, with encodeShareURL spied on): it('should not encode the share URL until the popover opens, so hovering the button does no compression'). No end-to-end test: the behaviour is unchanged, and the existing share-link specs cover the URL itself.
Suggested fix
Start shareURL as null and compute it only in the existing effect when the popover opens, showing a placeholder until then. The minimum fix is a lazy initializer, useState(() => encodeShareURL(...)).
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e).ShareButton.tsxhas not changed since this was filed.More compressions than the issue counts
ShareButton.tsx:25-36, and the re-render itssetShareURLcauses, which evaluates the eager argument again. Each Preview Mode toggle also costs three, and closing the popover costs one.IndexPagesubscribes tostore.instruments(IndexPage.tsx:23) and rendersHeader, which is not memoized.updateSelectedInstrument(SaveButton.tsx:32) writes a newinstrumentsarray. So every save of a media instrument pays for one compression it never uses. A language switch costs one too, becauseuseTranslationre-renders onlanguageChange.apps/playground/src/main.tsx:17wraps the app inStrictMode, so on the dev server every compression made during render runs twice. Measure on a production build.Fix
The placeholder isn't needed.
Popover.Contentunmounts its children while the popover is closed (RadixPresence). So move the popover body into aSharePopoverContentcomponent in the same folder and derive the link during render:That removes the
shareURLstate, the effect and its comment.ShareButtonno longer reads the files or the label. Opening costs one compression and so does each toggle. Hover, focus, Save and language switches cost none.Keep the
isFullscreenstate inShareButtonand pass it and its setter down. If it moves into the new component, Preview Mode resets on every open, which it does not do today. Readfileswith a selector, notuseFilesRef: that hook drops the unsubscribe function thatsubscribereturns (useFilesRef.ts:7-14), so a component that mounts on every open would add a listener each time.The lazy initializer on its own also stops the hover, Save and language compressions. It still compresses once at mount, and each open still renders twice.
Tests
ShareButton.test.tsxneeds// @vitest-environment happy-domas its first line, asapps/gateway/src/components/__tests__/Cap.test.tsxhas. Onmain,apps/playgrounddeclares none ofhappy-dom,@testing-library/reactor@testing-library/dom. All three are existing catalog entries. build: upgrade dependencies held back by breaking changes #1660 adds them toapps/playground/package.jsonand documents the docblock in the playgroundAGENTS.md## Testssection. Either land after build: upgrade dependencies held back by breaking changes #1660, or add them here and update that section yourself (fix: resolve 26 low-difficulty issues, one commit each #1825 edits it too). The store importssrc/instruments/index.ts, which top-level-awaits afetchof every binary example (lines 20-22), and it persists throughidb-keyval. Mock@/instrumentsandidb-keyvaland drive the realuseAppStore, as the slice tests that fix: resolve 26 low-difficulty issues, one commit each #1825 adds undersrc/store/slices/__tests__/do. Cover these cases: hover, focus and blur never callencodeShareURL; opening calls it once; the switch addsfullscreen=1.testing/src/specs/playground.spec.tsbuilds its link withgeneratePlaygroundURLand navigates to it. Add one that loads such a link, opens the popover and reads the#linkinput. It should check thatdecodeShareURLgives back the same label and files, then turn on thePreview Modeswitch and checkisFullscreenShareURL. The Share trigger has no accessible name until Playground icon-only buttons have no accessible names, and editor tabs cannot be closed from the keyboard #1763 is fixed, so either land after it or add the trigger'saria-labelhere. Put anopenSharePopover()method ontesting/src/pages/playground/index.page.ts. build: upgrade dependencies held back by breaking changes #1660 and fix(playground): search instruments by label #1817 both edit that file and the spec.Triage note written with Claude Code. Code references point at
main@d86740e.