- From: Nick Coury via GitHub <noreply@w3.org>
- Date: Mon, 10 Nov 2025 22:32:43 +0000
- To: public-css-archive@w3.org
My team is heavily supportive of exploring this. ## Our Motivation We have been using both same-document and cross-document View Transitions for a while now. As much as we tried to use the default main thread bound model, we found it completely unacceptable for virtually 100% of use cases as it would jank out in real world use cases. For cross-document VTs, the transition is playing during page instantiation when we are regularly doing heavy JS work along with the browser itself. Especially on low end devices and with many teams changing code, it's essentially impossible to prevent long tasks from making the transition choppy and dropping frames. For same-document VTs, the situation is both better and worse. Better in the sense that the feature triggering the transition is usually the owner of the code running and should be responsible for breaking up long tasks and ensuring performance. But it's worse in that they are used more often and the variety of transitions and features using them are greater, so it's harder to get all teams to always write performant code and transitions. We've even seen a pattern where optimizing for INP delays a bunch of work until future tasks. This tends to be things like content fetches for the end of the transition that can't always be delayed until the animation finishes. ## Our solution(s) Maybe unsurprisingly, very similar to the motion.dev approach. Our first approach was essentially www.bram.us/2025/02/07/view-transitions-applied-more-performant-view-transition-group-animations/ and rewriting keyframes on the fly. Though we've been having issues on Safari lately that seem to be stemming from modifying the default transitions, though we haven't confirmed the underlying issue yet. We've since been moving to an approach of doing the same measurements, then creating a fresh WAAPI animation that matches the default animation or our desired animation. In regards to the distortions, we've found there are multiple different approaches all with tradeoffs. We do one by default, but use a single enum to allow VT authors to pick a compositing mode: NONE - Don't modify the VT (fallback if anything goes wrong in calculations) SCALE (default) - Very similar to the default VT. It maintains aspect ratio and scales the before rect width to the after rect width. SCALE_HEIGHT - Same as SCALE but scales before height to after height. This has been useful for text, where maintaining height allows allows text of different lengths but the same number of lines to seamlessly morph. Otherwise, width morphing looks quite bad. SCALE_COUNTERSCALE - I believe this is what motion.dev is describing. We use SCALE on the view-transition-group, which normally would be distorted. We then compute a counter-scaling easing curve similar to what's described at https://developer.chrome.com/blog/performant-expand-and-collapse though it hand waives away the general solution which we had to solve. The math is a bit too complex to succinctly describe but could go into more depth if needed. Essentially we take the bezier curve or linear() curve from the existing easing, generate a large number of interpolated points, then at each point determine the value that will counter-scale it to keep aspect ratio constant. We then construct a counter-scale easing curve and apply it to the view-transition-image-pair which conveniently works for this purpose. It perfectly balances out the stretching down to pixel or subpixel precision. This also allows us to put `overflow: hidden` on the v-t-group which adds visual containment. SCALE_HEIGHT_COUNTERSCALE - Combines SCALE_HEIGHT with SCALE_COUNTERSCALE. STRETCH - Does the naive scaling math. This is often desirable when scaling a background that's a solid color, where the stretching is a feature and not a bug, and it's cheaper to calculate than counterscaling. SLIDE - Forces translate only and ignores scale. Could produce jank if things aren't the same size, but when they are supposed to be the same size this is both cheap and avoids any potential pixel distortion. This sometimes works better than others when there's a 1-2 px difference in sizes that isn't very apparent during the cross-fade, and it avoids bad visuals from unexpected scaling or stretching of text. Not confident this is an exhaustive list but this has covered all of our use cases so far. Border Radius - I believe it would be possible to do even more work to get this on the compositor with similar approaches, as the above counter-scale approach still distorts border radius. But given how subtle this effect is, we just animate a counter-scale on the main thread and that seems sufficient even if frames get dropped updating it. https://www.bram.us/2025/03/11/view-transitions-border-radius/ has some more details on how to perfect this which could be useful for any official implementation. Issues with this approach (beyond needing to decide on a conversion mode): - Can be expensive. We have to measure a bunch of things the browser already knew. We see the performance costs in our local profiling through style recalcs and forced layouts. - Getting the timing right on the measurements and animation modifications can be tricky, and may be the reason we're seeing issues in Safari. It's especially problematic because you have to measure after viewTransition.ready, but at this point the default animations are already in "play" mode. So if you're not careful you can be modifying running animations, or creating WAAPI animations that should be coordinated but don't start at the same time. Or, you have to pause and rewind default VT animations, then play with any additional animations to try and guarantee ideal behavior. It adds a lot of complexity and room for subtle bugs. - We have to re-implement a bunch of CSS parsing to extract easing values to do the counter-scale, which either involves a large library or becomes very brittle. - CSS counter-scaling calculations are really challenging to get right down to the pixel. Other observations: - SCALE_COUNTERSCALE behaves very similar to the default VT spec in most cases, and perhaps should be the default. But it doesn't always. In particular, we included STRETCH because setting an element to `width: 100%; height: 100%` on a default VT breaks visually unless you use STRETCH calculations. This highlights that you can't completely move to the compositor with 100% flexibility without changing some animations visually. But it is likely an edge case. Using the opt-in approach is a good way to balance this in the short term. - In the long term, I would love to consider how compositing could become the default. Other than breaking existing animations, it seems like being main thread bound by default does a disservice to View Transitions which are otherwise excellent, and it seems like an avoidable foot gun. As long as it's the default when VTs are written, it shouldn't cause any problems that aren't immediately identified. We especially see the jank on low end devices that developers often forget to test on. -- GitHub Notification of comment by nickcoury Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/13064#issuecomment-3514179045 using your GitHub account -- Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config
Received on Monday, 10 November 2025 22:32:44 UTC