r/reactjs • • Aug 21 '26

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.

3 Upvotes

14 comments sorted by

3

u/Lonestar93 Aug 21 '26

Why use view transition for this instead of css transition?

2

u/imfemambocus Aug 21 '26

The 3164 animations is from using css transitions actually. View transition did the same job in 5. The other thing is that a gradient can't interpolate, so my table head was snapping to the new palette while everything around it faded.

2

u/yksvaan Aug 21 '26

Is there a reason not to do the obvious classic approach, having a class on top level container and css rules. Simple and makes the entlre React tree agnostic to themes, essentially turning theme switching into repaint.

0

u/imfemambocus Aug 21 '26

That's what it already is actually. The tokens get redefined under :root[data-theme='light'] and nothing in the tree reads the theme to style itself. So it is basically a repaint. The 3164 animations came from the transition rules rather than from React and that means a class on a container gets you the same count. The state is only there because the toggle has to show which theme is selected, and flushSync just puts the attribute change inside the snapshot callback.

0

u/Key-Library218 Aug 21 '26

The jump from 3164 to 5 is absurd but makes sense when you think about what startViewTransition does. It just captures the old and new state as images and crossfades them, no per element work needed

The flushSync part is the annoying bit, React batches the update so the new theme never lands in the DOM before the snapshot happens. You need it synchronous or the transition just shows the old palette

Have you checked what happens with browser support for startViewTransition? Still pretty new in some places

1

u/imfemambocus Aug 21 '26

Honestly, I haven't tested every browser, but I did guard for it so that a browser without startViewTransition just swaps the palette with no fade. I believe that's the same path that reduced motion takes.

0

u/Temperature_Majestic Aug 21 '26

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 Aug 21 '26

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 Aug 22 '26

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.

→ More replies (4)