Closed vmpstr closed 2 years ago
I propose that we only update the styles once per frame, around rAF timing.
+1. In fact we should do after step 14 of update the rendering loop. This allows all script callbacks to run before we query the element's layout size for generating the pseudo-element style.
cause us to update the style & layout twice
The second style&layout is pretty cheap because it only needs to walk the pseudo-element tree. So I think that's reasonable.
We had an offline discussion about this today, driven by ongoing implementation work. I'm adding a summary of the discussion and the updated proposal:
When script invokes createTransition(), we wait until after ResizeObserver callbacks have been invoked (step 14) at the next update the rendering loop to execute the steps here. This includes identifying elements which will participate in a transition (based on page-transition-tag and other constraints) and caching pixels + other state for them.
The above is by design since we want to cache the state above based on what will be displayed to the user in that frame. Since ResizeObserver callbacks synchronously execute script, mutations for them have to be reflected in the cached state.
Once the frame above has been presented, the updateDOM callback is invoked (asynchronously) and we wait for it to settle. The task which runs when the promise returned by updateDOM resolves synchronously identifies elements which will participate in a transition in the new DOM. This implies forcing clean style/layout outside of a frame (update the rendering loop) but is necessary here to construct the pseudo-DOM. After this the ready
promise is resolved that developer can use to hook their customizations.
The tricky part here is that style on the pseudo-elements depends on style/layout of the corresponding DOM element. The spec text currently indicates that this is also updated only during update the rendering loop. So for example, the following code would result in an incorrect style value:
document.createTransition(...).ready.then(() => {
// Change the width of a tagged element.
target.style.width = "100px";
console.log(window.getComputedStyle(document.documentElement, "::page-transition-container(target)").style.width);
});
It's simpler to treat these pseudo-elements like any other element where from the developer's perspective, the style is always up to date once the ready
promise resolves and they are accessible in script. The user agent does whatever computation it needs to do (lazily) to ensure that.
We need to sort out what this means for UA animations (#7813) which will also be impacted by DOM mutations above.
t's simpler to treat these pseudo-elements like any other element where from the developer's perspective, the style is always up to date once the
ready
promise resolves and they are accessible in script.
To be pedantic here, there are two notions here: the script is up to date as specified in the UA stylesheet, and when that UA stylesheet is updated to reflect a changed source element. I agree that script should always observe values in the UA stylesheet when they getComputedStyle, and that something that already happens for all elements.
However, when the stylesheet is updated to represent the changed source element does not have a precedent, so treating this as we would in any other element doesn't mean much.
I think you're suggesting updating the UA stylesheet whenever script requests getComputedStyle, but I suspect that complicates the implementation quite a bit. I would still prefer to have an explicit step with specified timing that takes the changed source element values and updates the UA stylesheet
when the stylesheet is updated to represent the changed source element does not have a precedent
Conceptually generating style for these elements is similar to computing the layout for any element IMO. Let's take the ::page-transition-container as an example. The style values for it are computed based on the text here : "Set width to the current width of capturedElement’s incoming element's border box."
So the browser would need to update layout to get the corresponding element's layout bounds and generate the style values for this pseudo-element. A similar layout operation would've been forced if the developer queried offsetWidth for that DOM element. So why can't we do the same thing here if the developer queries style for that pseudo-element? Trying to limit this to specific events makes it difficult for the developer to reason about when to query this state.
Summarizing an offline discussion:
It's ok to synchronously generate the UA stylesheet for the pseudo DOM when the developer callback is done and before resolving the ready promise. This ensures that developers have a hook in script to customize based on this style information.
Proposed Resolution: Synchronously generate styles for pseudo-DOM after DomUpdateCallback and before resolving ready promise.
We will also need to re-generate the UA stylesheet after ResizeObserver (when all script mutations are done) to ensure the pseudo-elements reflect the state the DOM is painted with. For example if the DOM element's painting is at a size which is inconsistent with the pseudo-element's style (because its UA stylesheet is stale) then we'll get incorrect rendering.
Proposed Resolution: Generate styles for pseudo-DOM after step 14 of update the rendering of every frame after resolving ready promise.
It's unclear whether we should regenerate the UA stylesheet for all script APIs like getComputedStyle. The reason is implementation complexity. We'll need to run one style/layout pass over author DOM to compute the requisite state, then generate a UA stylesheet using these values, then trigger another style/layout pass for the pseudo-DOM.
The downside is that style values on the pseudo-element observed by script will always be a frame late. For example, in the following case the style value will be wrong:
function prepareFirstFrame() {
// Change the width of a tagged element.
target.style.width = "100px";
// This value is based on the stylesheet generated before resolving ready promise.
console.log(window.getComputedStyle(document.documentElement, "::page-transition-container(target)").style.width);
}
async function doTransition() {
await document.createDocumentTransition(...).ready;
requestAnimationFrame(prepareFirstFrame);
}
It's possible that the the first frame of the new DOM depends on script callbacks which run during update the rendering (like ResizeObserver). So the developer won't be able to use that information in customizing their animations.
Proposed Resolution: UA styles on the pseudo-DOM stay in sync with author DOM for any developer observable API after the ready promise is resolved.
The CSS Working Group just discussed when to update pseudo-element styles
, and agreed to the following:
RESOLVED: Synchronously generate styles for pseudo-DOM after DomUpdateCallback and before resolving ready promise.
RESOLVED: Update the UA stylesheet as needed in a new step after the current step 14 of "update the rendering" (after RO callbacks are done)
Adding Agenda+ to resolve on the third one:
Proposed Resolution: UA styles on the pseudo-DOM stay in sync with author DOM for any developer observable API after the ready promise is resolved.
The CSS Working Group just discussed [css-view-transitions-1] when to update the pseudo element styles
, and agreed to the following:
RESOLVED: UA styles on the pseudo-DOM stay in sync with author DOM for any developer observable API
Shared Element Transition spec describes pseudo elements that are containers, and that they should animate, for example, from the outgoing element's transform to the new element's transform.
Since this is done with CSS animations, we would need to keep these animations up to date to reflect any changes to the represented element.
Consider the animation is started, and then the represented element is moved around in a setTimeout. Does the animation update any time the represented element changes? Or is there a particular time at which we "update" the pseudo element styles?
I propose that we only update the styles once per frame, around rAF timing. This is still likely to cause us to update the style & layout twice, since we need the first time to determine the represented element's properties, then apply those to the pseudo elements which will cause a style invalidation and run a second style & layout to produce the visual frame.
The only way I can think of to avoid the "double render" is to always lag one frame behind when updating these pseudo element styles, but that doesn't seem ideal