How hard is it to compute a speculative style cascade ahead of time?

3 views
Skip to first unread message

Philip Rogers

unread,
Apr 16, 2025, 7:40:12 PMApr 16
to style-dev, Robert Flack, Stefan Zager, Rune Lillesveen
Style-dev,

We have a potential secondary launch approach for declarative interactions (details here), and it depends on being able to efficiently pre-compute the effect of a hypothetical css pseudo class change ahead of time. For example:
container chevron {
  transform: rotate(0);
  transition: transform 5s;
}
container:active chevron {
  transform: rotate(180deg);
}

I would like to set up a paused compositor transition for chevron from rotate(0) to rotate(180deg) in preparation for starting this transition on the compositor thread in response to a user interaction that would activate :active on container. There are many cases where this can get hard, such non-composited property changes (e.g., layout), precedence issues, and non-local effects (e.g., changes to styles on other elements), and I am okay with only creating my compositor transition for the easiest cases.

Doing a forced :active style update during the paint lifecycle phase would be problematic because this could dirty style/layout and cause additional frames, so I think this needs to either occur during style recalc, or in a way that doesn't dirty the real style/layout. Is there a way?

Chris Harrelson

unread,
Apr 17, 2025, 2:03:23 PMApr 17
to Philip Rogers, style-dev, Robert Flack, Stefan Zager, Rune Lillesveen
Maybe we can get part or most of the way to this goal with incremental changes, like:

* When an input event happens that will cause a state change, do the rendering update + early commit to start an animation when possible before running other tasks (we recently shipped a change to prioritize rendering in this situation)
* will-change already exists to reduce computation delay for starting the animation; consider adding more values if something is missing
* Add more pseudo classes and async events (similar to the `toggle` event for popover) that cannot be preventDefaulted / have async event handlers, to make it more foolproof for developers to start animations without initial jank
* Add passive event listeners (Scott is planning that for later this year)

The nice thing about these is they won't require interpreting gestures on the compositor, detecting whether potential state changes would cause animations that are compostable, or representing a paused animation on the compositor thread.

Chris

Stefan Zager

unread,
Apr 17, 2025, 2:49:54 PMApr 17
to Chris Harrelson, Philip Rogers, style-dev, Robert Flack, Rune Lillesveen
A few observations...

On Thu, Apr 17, 2025 at 11:03 AM Chris Harrelson <chri...@google.com> wrote:
Maybe we can get part or most of the way to this goal with incremental changes, like:

* When an input event happens that will cause a state change, do the rendering update + early commit to start an animation when possible before running other tasks (we recently shipped a change to prioritize rendering in this situation)

We are already getting pretty much all the benefit possible along these lines: after Scott's recent work, we pause non-rendering tasks on the main thread after an input event is processed, until the rendering update is done. We can't initiate a main thread rendering update before the compositor thread asks for it, because the compositor thread bundles essential information with the BeginMainFrame task.
 
* will-change already exists to reduce computation delay for starting the animation; consider adding more values if something is missing

will-change reduces the composited layer allocation work, but that work is unlikely to be a significant contributing factor to slow input response at the high percentiles. It is almost certainly dwarfed by the time spent running user script.
 
* Add more pseudo classes and async events (similar to the `toggle` event for popover) that cannot be preventDefaulted / have async event handlers, to make it more foolproof for developers to start animations without initial jank

This is definitely something we should investigate, but I'm not strongly confident that it will be feasible because I think it will be very difficult to accurately detect state changes in pseudo-states on the compositor thread. By way of contrast, I think detecting/interpreting input events would be much easier.
 
* Add passive event listeners (Scott is planning that for later this year)

100%, but I think this is orthogonal.

The nice thing about these is they won't require interpreting gestures on the compositor, detecting whether potential state changes would cause animations that are compostable, or representing a paused animation on the compositor thread.

Again, my sense is that interpreting gestures is challenging but not impossible; whereas detecting state changes in semantically-loaded pseudo-states would be much harder.

We already have the ability to represent paused animations on the compositor thread.

Chris Harrelson

unread,
Apr 17, 2025, 6:39:29 PMApr 17
to Stefan Zager, Philip Rogers, style-dev, Robert Flack, Rune Lillesveen
On Thu, Apr 17, 2025 at 11:49 AM Stefan Zager <sza...@google.com> wrote:
A few observations...

On Thu, Apr 17, 2025 at 11:03 AM Chris Harrelson <chri...@google.com> wrote:
Maybe we can get part or most of the way to this goal with incremental changes, like:

* When an input event happens that will cause a state change, do the rendering update + early commit to start an animation when possible before running other tasks (we recently shipped a change to prioritize rendering in this situation)

We are already getting pretty much all the benefit possible along these lines: after Scott's recent work, we pause non-rendering tasks on the main thread after an input event is processed, until the rendering update is done. We can't initiate a main thread rendering update before the compositor thread asks for it, because the compositor thread bundles essential information with the BeginMainFrame task.

+1, I agree that there may not be much we can do about this part.

* will-change already exists to reduce computation delay for starting the animation; consider adding more values if something is missing

will-change reduces the composited layer allocation work, but that work is unlikely to be a significant contributing factor to slow input response at the high percentiles. It is almost certainly dwarfed by the time spent running user script.

Sure. I was just saying that will-change would reduce the delay from not pre-positioning a paused animation in cc.


* Add more pseudo classes and async events (similar to the `toggle` event for popover) that cannot be preventDefaulted / have async event handlers, to make it more foolproof for developers to start animations without initial jank

This is definitely something we should investigate, but I'm not strongly confident that it will be feasible because I think it will be very difficult to accurately detect state changes in pseudo-states on the compositor thread. By way of contrast, I think detecting/interpreting input events would be much easier.

My suggestion is that we don't need to do that on the compositor thread - instead we can do the rendering update and commit of the animation start immediately after input. Maybe that overhead wouldn't be justified, but it seems straightforward to prototype?

* Add passive event listeners (Scott is planning that for later this year)

100%, but I think this is orthogonal.

I think it's relevant, because to the extent developers use them we can speed up animations from those input events, and delay the event listeners accordingly.

Also, I didn't know that we already have a way to model a paused animation in cc. Was that introduced because of scroll-driven animations?

Stefan Zager

unread,
Apr 18, 2025, 12:18:58 PMApr 18
to Chris Harrelson, Philip Rogers, style-dev, Robert Flack, Rune Lillesveen


On Thu, Apr 17, 2025, 3:39 PM Chris Harrelson <chri...@google.com> wrote:


Also, I didn't know that we already have a way to model a paused animation in cc. Was that introduced because of scroll-driven animations?

No, the 'paused' keyword has been part of the web animations spec all along, and cc already handles it appropriately.

Stefan Zager

unread,
Apr 18, 2025, 12:21:34 PMApr 18
to Chris Harrelson, Philip Rogers, style-dev, Robert Flack, Rune Lillesveen

Rune Lillesveen

unread,
Apr 24, 2025, 3:33:40 AMApr 24
to Philip Rogers, style-dev, Robert Flack, Stefan Zager
On Thu, Apr 17, 2025 at 1:40 AM Philip Rogers <p...@google.com> wrote:
Style-dev,

We have a potential secondary launch approach for declarative interactions (details here), and it depends on being able to efficiently pre-compute the effect of a hypothetical css pseudo class change ahead of time. For example:
container chevron {
  transform: rotate(0);
  transition: transform 5s;
}
container:active chevron {
  transform: rotate(180deg);
}

I would like to set up a paused compositor transition for chevron from rotate(0) to rotate(180deg) in preparation for starting this transition on the compositor thread in response to a user interaction that would activate :active on container. There are many cases where this can get hard, such non-composited property changes (e.g., layout), precedence issues, and non-local effects (e.g., changes to styles on other elements), and I am okay with only creating my compositor transition for the easiest cases.

Maybe, if you can ignore cascading and effects on other elements. As long as you resolve style for a single element without setting the resulting style on the Element/LayoutObject and make sure any created animation effects are isolated from the "normal" ElementAnimations.

I'm not saying it's not quite a bit of work.

Have you considered a separate CSS syntax for such animation effects to make it clear to the author that there is a limited and well-defined set of what you can do?

Doing a forced :active style update during the paint lifecycle phase would be problematic because this could dirty style/layout and cause additional frames, so I think this needs to either occur during style recalc, or in a way that doesn't dirty the real style/layout. Is there a way?

Not out-of-the-box readily available API, but I think the way I imagine it could be done it wouldn't need to happen during the style recalc traversal. E.g. a StyleResolver API that is called for a single element, that temporarily forces the active-state to true (like devtools can), does the cascading and computes the ComputedStyle, and stores the new animation effects isolated from the "normal" effects that I mentioned earlier, so that you can pass those separate effects to the compositor somehow.

--
Rune Lillesveen

Philip Rogers

unread,
Apr 24, 2025, 6:44:53 PMApr 24
to Rune Lillesveen, style-dev, Robert Flack, Stefan Zager
Thanks Rune! Based on your response, I think the implementation work involved in the pseudo class approach will be too high to favor it over the easier-to-implement animation-trigger approach.

On Thu, Apr 24, 2025 at 12:33 AM Rune Lillesveen <fut...@google.com> wrote:
On Thu, Apr 17, 2025 at 1:40 AM Philip Rogers <p...@google.com> wrote:
Style-dev,

We have a potential secondary launch approach for declarative interactions (details here), and it depends on being able to efficiently pre-compute the effect of a hypothetical css pseudo class change ahead of time. For example:
container chevron {
  transform: rotate(0);
  transition: transform 5s;
}
container:active chevron {
  transform: rotate(180deg);
}

I would like to set up a paused compositor transition for chevron from rotate(0) to rotate(180deg) in preparation for starting this transition on the compositor thread in response to a user interaction that would activate :active on container. There are many cases where this can get hard, such non-composited property changes (e.g., layout), precedence issues, and non-local effects (e.g., changes to styles on other elements), and I am okay with only creating my compositor transition for the easiest cases.

Maybe, if you can ignore cascading and effects on other elements. As long as you resolve style for a single element without setting the resulting style on the Element/LayoutObject and make sure any created animation effects are isolated from the "normal" ElementAnimations.

The cascade would be required, I think. Our current proposal looks like this:
<!doctype html>
<style>
  @keyframes spin {
    from { rotate: 0deg; }
    to { rotate: 180deg; }
  }
  .title {
    animation-trigger-name: spin-trigger;
  }
  .chevron {
    animation: spin 0.25s forwards paused;
    animation-trigger: pointerdown(spin-trigger) once;
  }
</style>
<div class="title">title</div>
<svg class="chevron" style="width: 24px; height: 24px;">
  <path d="M16.59 8.59 12 13.17 7.41 8.59 6 10 12 16 18 10Z"></path>
</svg>

And the alternative pseudo class approach looks like:
<!doctype html>
<style>
  .title:active ~ .chevron {
    transform: rotate(0deg);
  }
  .chevron {
    transform: rotate(180deg);
    transition: transform 0.25s;
  }
</style>
<div class="title">title</div>
<svg class="chevron" style="width: 24px; height: 24px;">
  <path d="M16.59 8.59 12 13.17 7.41 8.59 6 10 12 16 18 10Z"></path>
</svg>

To "convert" between these, we need to be able to calculate the styles applied to .chevron, assuming .title is :active. Given what you said ("Maybe, if you can ignore cascading and effects on other elements"), this seems non-trivial.

Rune Lillesveen

unread,
Apr 25, 2025, 3:48:01 AMApr 25
to Philip Rogers, style-dev, Robert Flack, Stefan Zager
Right. To cover the adjacent combinator case you'd basically need a full recalc pass.
 


I'm not saying it's not quite a bit of work.

Have you considered a separate CSS syntax for such animation effects to make it clear to the author that there is a limited and well-defined set of what you can do?

Doing a forced :active style update during the paint lifecycle phase would be problematic because this could dirty style/layout and cause additional frames, so I think this needs to either occur during style recalc, or in a way that doesn't dirty the real style/layout. Is there a way?

Not out-of-the-box readily available API, but I think the way I imagine it could be done it wouldn't need to happen during the style recalc traversal. E.g. a StyleResolver API that is called for a single element, that temporarily forces the active-state to true (like devtools can), does the cascading and computes the ComputedStyle, and stores the new animation effects isolated from the "normal" effects that I mentioned earlier, so that you can pass those separate effects to the compositor somehow.

--
Rune Lillesveen



--
Rune Lillesveen

Reply all
Reply to author
Forward
0 new messages