Repository navigation
fix(ui): cut the main thread cost of working Wisps and panel drags - #332
Merged
Merged
Conversation
This was referenced Oct 31, 2026
A working Wisp took about a fifth of the main thread while replies streamed in. Chromium lays out every frame for the individual rotate, translate, and scale properties on SVG elements, but runs transform off the main thread, so the Wisp's keyframes and blink now use transform. Dragging a panel edge set state at the top of the app on every move, which rendered everything and went over the frame budget. The drag now writes to the panel's custom property and commits when it ends. The recording meter and connection bar stop animating layout too. The meter's bars lose their 60ms tween, which ran constantly at that sample rate and cost more than the layout it would have replaced.
gustmrg
force-pushed
the
fix/composited-wisp-motion
branch
from
November 1, 2026 03:13
77021b9 to
7aaee14
Compare
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.
Stacked on #330.
While a Wisp worked, its movement took about a fifth of the main thread, right when replies are streaming in. Dragging a panel edge rendered the whole app on every move and went over the frame budget. Both now cost far less and look the same. The recording meter and the connection bar also stopped animating layout, and the meter's bars no longer tween between samples.
Causes and fixes
rotate,translate, andscaleproperties on SVG elements, but runstransformoff the main thread.wisp-sway,wisp-look,wisp-bob, andBLINK_KEYFRAMESnow usetransform.will-changeandcontainmade no difference, so neither was added.useResizablePanelcalledsetWidthon everypointermove, at the top ofApp. It now writes to the panel's custom property, named by the newcssVariableinResizablePanelLayout, and commits to state when the drag ends.scaleYinstead ofheight. The 60ms transition is gone because it ran constantly at the 60ms sample rate: keeping it withscaleYcost more thanheightdid, since all 100 bars animated at once.scaleXinstead ofwidth, and the shimmer moves an::beforegradient withtransforminstead of animatingbackground-position.Main thread time, before and after
These are single runs under xvfb on a desktop, so read them as ratios. A phone will differ.
Testing
npm run typecheck,npm run lint,npm run format:check, andnpm run buildpass. Lint reports the same 2 warnings asmain.npm teston Node 22: 2449 tests pass.use-resizable-panel.test.ts: a drag doesn't render until it ends. The existing tests now read the width from the panel during a drag.data-state="working"on two Wisps. Five screenshots of the header Wisp over two seconds all differ, so it still moves off the main thread.transform, on::before.To check by hand before merging:
Not in this PR
useRevealedTextparses a reply's Markdown up to 30 times a second, a limit that was set on purpose. A long reply may still drop frames, which needs a recording of a real reply.ui/accordion.tsxanimatesheightbecause its content has an unknown size. That one stays.Refs SWE-268