Skip to content

[css-view-transitions-1] Should the DOMChangeCallback be posted as a task from skip transition steps? #7904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
vmpstr opened this issue Oct 18, 2022 · 13 comments · Fixed by #8827
Labels
css-view-transitions-1 View Transitions; Bugs only Needs Edits

Comments

@vmpstr
Copy link
Member

vmpstr commented Oct 18, 2022

Step 4 in skip the page transition algorithm says that the DOM change callback has to be invoked synchronously. However, the skip the page transition algorithm sometimes runs as part of the update the rendering, and we really want to avoid running (new) script in those steps.

We should make it asynchronous. The question is should we just make it asynchronous for all places that run the skip the page transition algorithm, or should it be conditional?

I'm leaning towards just make it all asynchronous, but I'm unsure if there are cases where synchronous invocation is important.

/cc @khushalsagar @jakearchibald

@vmpstr vmpstr added the css-view-transitions-1 View Transitions; Bugs only label Oct 18, 2022
@khushalsagar
Copy link
Member

The downside of making it async is the following sequence:

  • Queue a transition with createDocumentTransition.
  • While snapshot is pending (so the callback hasn't run) initiate a new transition so the previous one is skipped.

It would be nice if the callback for the previous one consistently happened before or after we capture for the new one. But that depends on whether the event loop already has a task to run the next frame? I think the answer to this is a microtask which would ensure that this runs before the next frame. @flackr on that.

Microtask won't help if multiple transitions are set up within the same frame but that seems like an edge case that the developer can handle better.

@khushalsagar
Copy link
Member

Summary from offline discussion. Microtasks need a checkpoint to ensure they run before a specific event. Since there is already a checkpoint for Web Animations, using a microtask will ensure the callback runs before the next rAF. It is brittle to rely on an unrelated checkpoint for this timing though.

Also since the callback itself is async, it's already possible that it hasn't finished before the next rAF. The domUpdated promise was added precisely so developers have a hook to know when the callback is done. That could be used to delay queueing another transition if it should deterministically start after the callback for the first one is done.

Proposed Resolution: Invoking the dom-update-callback is done by positing a task in skip the page transition.

@ydaniv
Copy link
Contributor

ydaniv commented Oct 28, 2022

If I'm following correctly the proposal is to always post as microtask?
In any case it must be consistently one of sync/next task/next microtask to be predictable for authors and also mockable for testing.

@khushalsagar
Copy link
Member

The proposal is to use a task instead of microtask. Microtask timing can be unpredictable since the microtask checkpoint which runs them can be triggered by any feature.

@khushalsagar khushalsagar added the Async Resolution: Proposed Candidate for auto-resolve with stated time limit label Feb 7, 2023
@w3c w3c deleted a comment from css-meeting-bot Feb 21, 2023
@astearns
Copy link
Member

@ydaniv (and/or anyone else) are you OK with the clarification and proposed resolution?

@ydaniv
Copy link
Contributor

ydaniv commented Feb 25, 2023

@astearns SGTM!

@astearns
Copy link
Member

astearns commented Mar 7, 2023

OK, seeing consensus here so this should get in the spec for wider review.

The CSSWG will automatically accept this resolution one week from now if no objections are raised here. Anyone can add an emoji to this comment to express support. If you do not support this resolution, please add a new comment.

Proposed Resolution: Invoking the dom-update-callback is done by positing a task in skip the page transition.

@astearns astearns added Async Resolution: Call For Consensus Resolution will be called after time limit expires and removed Agenda+ Async Resolution: Proposed Candidate for auto-resolve with stated time limit labels Mar 7, 2023
@astearns
Copy link
Member

RESOLVED: Invoking the dom-update-callback is done by positing a task in skip the page transition.

@noamr
Copy link
Collaborator

noamr commented May 7, 2023

This would mess up the phases. By making update the callback async we make it happen after the ready/finish phases. Integrating it as per the resolution would assert at call the update callback step 1 (wrong phase).

Note that the change callback is called synchronously during the update phase also when it's not skiped, when preparing the transition.

IMO the callback should be called synchronouosly or the whole state machine becomes botched.

@khushalsagar
Copy link
Member

IMO the callback should be called synchronouosly

That's not feasible because we can execute this step very late in the update the rendering steps. Specifically, step 8.7.5 :

If transition’s initial snapshot root size is not equal to the snapshot root size, then skip the view transition for transition, and return.

The "Handle transition frame" algorithm is triggered here, right before step 16. The goal behind running it this late is that its supposed to reflect DOM changes into the corresponding pseudo-elements. So we want to run it at a point when script is guaranteed to not modify the DOM. But that also means we're at a point where running script is bad. It won't be logically incorrect for this feature but adding a hook where script can run implies the DOM can be dirtied and we'll need to re-run style/layout again.

We can make the async approach by doing the following:

  • In step 8.4.1 assert that phase is either before update-callback-called, which means this is running in the regular transition flow. Or assert that its done, which means this is happening because the transition was skipped before the callback was invoked.
  • Make step 8.4.5 conditional to only update the phase if it was before update-callback-called.
  • Add step 8.4.7 to invoke the finished promise if the phase is done. This means we delay dispatching the finished promise until the update callback is done if we're skipping the transition.
  • Step 8.5.4 will post a task to call the update function.
  • In step 8.5.9, don't resolve the finished promise if this task was posted.

I'll try to dig up the exact issue but IIRC we resolved on the fact that ready promise should reject immediately if you skip before call the update callback but finished should mirror the result of that callback.

@noamr
Copy link
Collaborator

noamr commented May 9, 2023

IMO the callback should be called synchronouosly

That's not feasible because we can execute this step very late in the update the rendering steps. Specifically, step 8.7.5 :

If transition’s initial snapshot root size is not equal to the snapshot root size, then skip the view transition for transition, and return.

The "Handle transition frame" algorithm is triggered here, right before step 16. The goal behind running it this late is that its supposed to reflect DOM changes into the corresponding pseudo-elements. So we want to run it at a point when script is guaranteed to not modify the DOM. But that also means we're at a point where running script is bad. It won't be logically incorrect for this feature but adding a hook where script can run implies the DOM can be dirtied and we'll need to re-run style/layout again.

I don't get why running scripts here is bad? It's not different from running scripts in resize/intersection observers/rAF.

What adding a postTask here would cause is a potential flicker. you'd have a moment where on one hand rendering is allowed ("transition suppressing rendering" is off), and OTOH the update callback wasn't called.

In the current spec, from the moment you start a transition it is guaranteed that there's no intermediate rendering until the transition is resolved. That's great. With this change, there would be this uncanny-valley moment of "the previous transition has not resolved but rendering is allowed". Of course, we could also put step 5 (Set transition suppressing rendering to false) in that task, but what would we gain? Allowing event/timeout handling while rendering is suppressed, which has to now take the intermediate suppressed state into account?

If we suppress rendering anyway, we might as well do everything synchronously and perform an additional style and layout if needed.

@khushalsagar
Copy link
Member

What adding a postTask here would cause is a potential flicker. you'd have a moment where on one hand rendering is allowed ("transition suppressing rendering" is off), and OTOH the update callback wasn't called.

Just reiterating the case which could potentially flicker:

function cancelTransition(transition) {
  // Author assumes that update callback was dispatched synchronously here.
  transition.skipTransition();

  // Author updates DOM assuming the update callback was already run.
  // This causes a flicker when the update callback is dispatched.
  updateDOMAfterTransition();
}

The update callback itself is async. In the case above, even if dispatched the update callback synchronously in skipTransition, the author still has to ensure its completed before running updateDOMAfterTransition. They can use updateCallbackDone promise for it.

function cancelTransition(transition) {
  transition.skipTransition();
  transition.updateCallbackDone.then(updateDOMAfterTransition);
}

Also note that we don't want to suppress rendering until the callback finishes if the transition was skipped. We don't know if the author will make the updateDOM callback a no-op in this case.

@noamr
Copy link
Collaborator

noamr commented May 10, 2023

Thanks @khushalsagar, this gives me enough context, I'll proceed with a PR based on the resolution.

noamr added a commit to noamr/csswg-drafts that referenced this issue May 11, 2023
Amend assert to allow calling the callback from a "done" state

The "finished" promise is bound to the update callback promise, so
it doesn't need to change (the update callback is async anyway).

See [resolution](w3c#7904 (comment)).

Closes w3c#7904
khushalsagar pushed a commit that referenced this issue May 11, 2023
…te callback from a task (#8827)

* When skipping transitions, call the DOM-update callback from a task

Amend assert to allow calling the callback from a "done" state

The "finished" promise is bound to the update callback promise, so
it doesn't need to change (the update callback is async anyway).

See [resolution](#7904 (comment)).

Closes #7904

* Add changelist entry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-view-transitions-1 View Transitions; Bugs only Needs Edits
Projects
None yet
5 participants