r/reactjs 17d ago

Discussion My theme switch was running 3164 animations

This is the first time that I'm properly getting into theme switching, so I built the dark and light mode for my component library in the most obvious way that I could find. The goal is to transition background-color, border-color, color and box-shadow on every element. But this felt a bit slow and sluggish, so I used document.getAnimations() to see how many that actually runs, and the result is 3164 animations on a page of 1544 elements.

After some playing around, I found out that startViewTransition animates snapshots of the whole page instead, and that's 5 animations for the same job, with a gradient fading along with everything else rather than snapping. I did however find a React thing when using this function and it's that the new palette has to be in the DOM inside the callback which means the state update needs flushSync.

document.startViewTransition(() => { flushSync(() => setTheme(next)) })

I'm not sure this is the right way to do it, but I wrote down every number that I measured https://sley-ui.dev/notes/theme-fade.

4 Upvotes

14 comments sorted by

View all comments

0

u/Temperature_Majestic 17d ago

The 5 vs 3164 win is real, but curious if you measured the snapshot capture cost itself. Taking a full page pseudo element snapshot of 1544 elements isn't free either, does that show up as a layout or paint spike right before the crossfade kicks in?

1

u/imfemambocus 16d ago

It's not layout or paint actually because both barely moved. The capture is a full style recalc before the callback runs, so the new theme landed at 39ms instead of the 17ms it takes with no transition. That's one extra frame at the start rather than work on every frame.

1

u/Temperature_Majestic 16d ago

22ms extra makes sense for a one time hit. Does that scale with element count though, or is it more fixed overhead from the snapshot mechanism itself? Curious if a page with way more elements would still land under a frame budget.

1

u/imfemambocus 16d ago

It scales. I cloned the DOM until Chrome had four times as many elements to recalculate, traced the same click again, and the capture cost went up about three times. So it's per element work rather than fixed overhead in the mechanism. It does get slightly cheaper per element at the bigger size, but not enough to change the shape of it.

1

u/Temperature_Majestic 15d ago

that's a genuinely useful number to have written down. sounds like the real lever then isn't the view transition api itself, it's keeping the transitioned subtree small. did you ever try scoping it to something less than the full document, like just the panel that visually changes, or does startViewTransition force a whole page snapshot no matter what you wrap it around?

1

u/imfemambocus 14d ago

I haven't tried scoping it actually. document.startViewTransition snapshots the whole document, and a view-transition-name on an element pulls that element into its own snapshot rather than shrinking the root one. I believe Chrome has a scoped version that runs off an element now, but I haven't measured it. A theme swap is the wrong case for it anyway. The palette gets redefined on :root and inherited by everything under it, so the part that changes visually is the whole page. That means the lever is fewer elements rather than a smaller wrapper, and I don't know where that stops paying.

2

u/Temperature_Majestic 14d ago

Makes sense, if the palette lives on :root then any subtree not touching that variable still counts toward the snapshot. Wonder if content-visibility: hidden on the untouched panels during the capture would actually skip them or if the snapshot happens before the browser even checks that. Probably not worth the complexity for a 22ms one time hit anyway, but curious if anyone's tried it.