[View Transition] Event on old Document to set transition state
Cross document VT needs an event on the old Document so authors can set up view-transition-names based on where the user is navigating to. The navigate event seems like the perfect choice but it has the following issues:
- It doesn't tell the final URL (after redirects) so authors will have to set state based on initial URL. We can't do this if there are cross-origin redirects (that won't allow a transition anyway) for security reasons but it can be considered for same-origin redirects. I don't know how significant that use-case is.
- It doesn't fire for all browser initiated navs, looks like its meant to be only for back/fwd buttons. We discussed whether all browser initiated navs should allow View Transitions at CSSWG. For example if the user enters a same-origin URL in the omnibox. And the resolution was to let the browser decide this: https://github.com/w3c/csswg-drafts/issues/8783. So if there is any nav which supports transitions, it will need the event.
- The event fires too early, when the nav is initiated. The user can continue interacting with the page before we have a server response. For example you click a link and continue to scroll the current page or click something which expands a menu. These interactions could require a change in the transition. Authors could work around this by tracking these updates themselves, like a scroll listener but better if we can give them an event right before the frame which is captured.
@noamr @vmpstr @domenic
We also have things like pagehide and beforeunload that are closer to the right time, and I believe are always dispatched (Although there are some awkward interactions with bfcache).
Are these events correct in their time? If not, is there a specific time during the document lifecycle where this event has to be dispatched?
Yeah, when you described the conditions in the OP, I think unload and pagehide (not beforeunload) are much closer to what you're looking for. Those are the events that fire when we actually have the post-redirects HEAD response in hand and are ready to destroy the current Document.
pagehide seems like the right one to me as well, just need to check if its guaranteed to be dispatched before the new Document starts executing script. I'm not sure about that.
We likely don't do it for cross-origin navs, since it will delay the navigation but can make an exception if its a same-origin nav with a transition?
just need to check if its guaranteed to be dispatched before the new Document starts executing script.
For non-prerendering cases, it is guaranteed. New documents don't start executing script until https://html.spec.whatwg.org/#scripts-may-run-for-the-newly-created-document
pagehideseems like the right one to me as well, just need to check if its guaranteed to be dispatched before the new Document starts executing script. I'm not sure about that.
For same origin navs it does.
For same origin navs it does.
Great!
Documenting a point @noamr made in our offline discussion, whatever event authors use will be followed by a frame, so that the names they setup influence what's captured. And it would be odd to render a frame after pagehide. I'm not sure if there's any API contract like this.
One model I'm gravitating towards is:
- Authors set up the opt-in config (to decide whether there will be a transition) using the
navigateevent. If that needs to change based on redirects we can explore adding events that informs the page of redirects going forward. - Dispatching a VT specific event right before the frame that will be captured? So the authors knows the exact DOM state that will be captured and names can be set accordingly.
How does this model work with prerendering? I know combining prerendering and MPA view transitions is a goal we all share :)
How does this model work with prerendering? I know combining prerendering and MPA view transitions is a goal we all share :)
For sure! I'm not seeing which aspect is ambiguous with respect to pre-rendering. Mind clarifying the question you had in mind?
Well, I guess I'm not 100% clear on the model, but I was scared by the question about whether pagehide (or any similar event) is "guaranteed to be dispatched before the new Document starts executing script". That's not going to be true in prerendering cases; the new Document will start executing script at an arbitrarily-early time.
Does that mess up the model here in any way? Your description in https://github.com/whatwg/html/issues/9702#issuecomment-1715384708 seems to be entirely about the preceding page, so maybe it's fine? But I wanted to double-check.
There is no inherent reason to perform this before the new document can run any script, but we didn't want to make this invoke an implicit type of prerendering (having two "live" pages without explicitly using speculationrules). It's OK if those events run after scripts have run in the prerender case
It's OK if those events run after scripts have run in the prerender case
+1, I now see my statement was confusing. Clarifying the model to make sure I got it right:
- We need to recommend a hook that authors use on the old Document to set up names. This is before the Document renders its last frame which is captured as snapshots.
- The new Document should be made visible after the step above. So whatever event devs will use to set up animations on the new Document must happen after 1 above. That'll likely be the reveal event: https://github.com/whatwg/html/issues/9315.
Going back to this, I think the event should not be specific to view transitions, but rather a counterpart to pagereveal on the old document, that allows:
- knowing the new URL, post same-origin
- affect capturing of the old state
I see 3 options here:
- Fire a new event on navigation commit, that's guaranteed to happen before a last capture frame. such event is likely VT-specific.
- Fire an event for each same-origin navigation redirect. This allows the old document to prepare early rather than wait till the last moment.
- Change the VT model so that the last capture is done after we send the
pagehideevent, and add some info topagehide.
All of the above shouldn't fire for prerender, only for a regular navigation redirect/commit.
I'm currently slightly leaning towards 2 but if we can find a way to make (3) not break things I'm open.
I actually prefer (1) without making it VT-specific. It could be a counterpart to the navigate event in the navigation API except this one is not cancellable.
The con with (2) is that it doesn't cover the other use-case for this event: "The user can continue interacting with the page before we have a server response." Imagine that the transition depends on scrolling which changes which elements are onscreen. Without an event which fires right before browser capture, authors need to run a scroll handler to keep the DOM in "capture ready" state until snapshot or navigation cancellation. I also don't think there is work the browser can "prepare early". Other than changing layerization decisions based on view-transition-names we try to defer the heavy rendering work until the last frame.
(3) is interesting but could run into assumptions that pagehide is dispatched after the Document is made invisible? Also it's likely fired on tab switching too. The navigation API seems like a better place for this info.
pagehide isn't fired on tab switching. pagehide is like the better version of unload event.
Thanks for clarifying @smaug----. Do you have thoughts on the other point: "rendering a frame after pagehide"? Would that mismatch developer expectations of how pagehide behaves today?
We discussed with @petervanderbeken, and if possible would prefer reusing pagehide. It is after all not that different from (2) in case there are no redirects. And per spec page visibility is changed to hidden after firing pagehide. Adding yet another unload type of event would be a bit unfortunate.
The extra rendering is a new thing anyhow. If the old page is rather static, it might currently not trigger any paints while the new page is loading. But, do we want that the extra rendering triggers all the relevant callbacks? And what should those callbacks be able to do? What if one triggers new fetches? Or loads something using sync XHR even? Or tries to navigate to some other page?
But, do we want that the extra rendering triggers all the relevant callbacks?
We need the update the rendering loop to run for snapshots. That's because if the author changes which elements have a view-transition-name, it changes layerization and needs the engine to push a new frame to the compositing stack.
And while we can suppress callbacks that run as a part of update-the-rendering, it doesn't help make the script interaction any simpler. Script can trigger anything with a setTimeout instead. But I see your point that we should probably disallow triggering a new navigation once we've begun capturing the old Document for a transition. Is there already a way to place the Document in this state? I'm guessing we similarly don't want script to do this in the pagehide event?
And per spec page visibility is changed to hidden after firing pagehide.
The challenge here is that we'll need to add an async step between firing the pagehide event and marking the page hidden. So we can produce one last frame to snapshot before the page is marked hidden. In the current spec marking the Document hidden is synchronous after pagehide dispatch here and is a part of unloading the Document. So there's no opportunity to produce another frame after dispatching pagehide.
That's why we intentionally added this async step earlier here, before the unload algorithm is triggered. Since there's already an async task to run step 11, it was easier to add a new async step for rendering and capturing the last frame at the beginning. And we can introduce a new event at this spot for the author to configure state that affects the captured frame.
Why would there be async step between firing pagehide and marking page hidden. We can have a rendering step there if we want. (I'm not saying we should do that, but just that we can, if we want)
Why would there be async step between firing pagehide and marking page hidden. We can have a rendering step there if we want.
Because update-the-rendering steps run on a new task (generally driven by platform vsync notifications). So we'd need to go back to the event loop after firing pagehide, wait for the next update-the-rendering loop and then continue with the unload steps starting with marking the Document hidden. Are you suggesting synchronously triggering update-the-rendering after dispatching pagehide?
I'm just saying that it would be possible, if we want that.
I'm just saying that it would be possible, if we want that.
Its way too complicated to support in Chromium. We have a pull model where the compositor pulls an update from Blink while this will require Blink pushing a frame to the compositor which is a fundamental change. So I would prefer keeping the capture step async.
Summarizing internal discussion:
Fitting in an "update the rendering" phase (and thus a task) between pagehide and visibilitychange would break existing author assumptions, that pagehide is happening in the last task of the page. e.g. you might be tearing down things in pagehide if not persisted, and having a rAF callback afterwards would be buggy.
Thus perhaps a pagewillhide or a outboundviewtransition event with a ViewTransition object (similar to pagereveal) would work the best. Unlike pagereveal, it's ok if this event is sent only when there is an actual view transition.
At which point would pagewillhide fire?
At which point would pagewillhide fire?
When we get the final response for the new document, it will fire, delay hiding/unloading and render one more frame if there is a view transition. If there is no vt it will fire right before pagehide.
Note that this event should include the URL of the target page in case it's same-origin. In general this event probably only makes sense for same-origin navigations, as in cross-origin navigations you can't delay the commit and you don't have a ViewTransition anyway.
Perhaps beforepagehide to match beforeunload, or beforepagetransition (note that pagehide is a PageTransitionEvent`), and have it only for actual same-origin page transitions.
beforeunload timing is quite different. That happens when you're about to start loading the next page. This new event would fire when the new page is about to be shown to the user. But pagehide and unload event timing is basically the same.
So perhaps better to not use name beforepagehide.
I'm still trying to figure out the timing of the new event from implementation point of view. In Gecko we'd need to do one extra parent process -> content process -> parent process round trip for this, I think. And I guess check whether window.stop() is called or new navigations started or history.go() used... hmm, couldn't sites misuse the event?
beforeunload timing is quite different. That happens when you're about to start loading the next page. This new event would fire when the new page is about to be shown to the user. But pagehide and unload event timing is basically the same.
So perhaps better to not use name beforepagehide.
How about beforepagetransition?
I'm still trying to figure out the timing of the new event from implementation point of view. In Gecko we'd need to do one extra parent process -> content process -> parent process round trip for this, I think.
Sounds right.
And I guess check whether window.stop() is called or new navigations started or history.go() used... hmm, couldn't sites misuse the event?
Yes, that's why I'm thinking to fire it for same-origin navigations only, or even fire it only when you have a pending cross-document view transition.
I want to make progress on this. My main contemplation is where this should be a document event like pagehide or in the navigation namespace.
The way it's going to work:
- When response headers come and we're about to unload and switch to a same-origin document:
- Fire this event, with:
- View transition object if there's an outbound view transition.
- URL you're going to
- Navigation type
- If there's an outbound view transition, wait until capture before running the remaining steps. During this wait the developer still has a chance to cancel the navigation, skip the transition etc.
- Unload the old document as usual
- Activate the new document as usual
- Fire this event, with:
So thinking navigation.oncommit, or document.onbeforepagetransition, or document.onpageconceal (to be symmetrical with pagereveal).
@smaug---- @domenic thoughts?
I think it would be most helpful to me to fully tease out the difference between this proposal, and the existing pagehide/unload events. In particular, why couldn't we just add some more fields to pagehide? Here is what I'm hearing so far:
If there's an outbound view transition, wait until capture before running the remaining steps. During this wait the developer still has a chance to cancel the navigation, skip the transition etc.
Can you describe this "capture" process more? I guess it is async, and JavaScript code can run during that time?
So this is kind of introducing a new async stage right before pagehide/unload. (But I guess the async stage is not that new, since fetching a new document is already async.) Sounds like that will be a bit of a pain to spec :)
Given this new async capture stage, I guess pagehide would be too late, since we need to fire this event at the beginning of the capture stage, and pagehide is at the end? Does pagehide need to be at the end of the capture stage, or could we place this new capture stage after pagehide?
Another difference I'm hearing is that this event won't fire for unloads that aren't part of a cross-document navigation. E.g., closing a window. That's a fairly big difference, although I'm not sure how important it is.
Some early thoughts on ergonomics:
-
The navigation API generally tries to treat same- and cross-document navigations on roughly equal footing, with a bias toward same-document navigations if anything. So this event would feel a bit out of place there. But maybe that could be ameliorated with a clear name, e.g.
oncrossdocumentcommit. -
I like the symmetry of
pageconceal/pagereveal. It might be a bit confusing that it doesn't fire on cases like closing a window, but maybe it's OK. -
It's not clear exactly what the proposed behavior is for when we're about to unload and switch to a cross-origin document. Event fires, with some censored properties? Event doesn't fire?
-
It might be nice if the properties of this event were a subset of
NavigateEvent, e.g.navigationType,destination,hasUAVisualTransition, maybeformData.
I think it would be most helpful to me to fully tease out the difference between this proposal, and the existing
pagehide/unloadevents. In particular, why couldn't we just add some more fields topagehide? Here is what I'm hearing so far:If there's an outbound view transition, wait until capture before running the remaining steps. During this wait the developer still has a chance to cancel the navigation, skip the transition etc.
Can you describe this "capture" process more? I guess it is async, and JavaScript code can run during that time?
Yes, it waits for an additional "update the rendering step", which would capture elements into images at the end and pass these images to the new document as a new ViewTransition object in that document.
So this is kind of introducing a new async stage right before pagehide/unload. (But I guess the async stage is not that new, since fetching a new document is already async.) Sounds like that will be a bit of a pain to spec :)
It's actually not too bad! see the first monkey patch here. When we get the final response headers we delay the rest of the steps and introduce an async step in the middle.
Given this new async capture stage, I guess
pagehidewould be too late, since we need to fire this event at the beginning of the capture stage, andpagehideis at the end? Doespagehideneed to be at the end of the capture stage, or could we place this new capture stage afterpagehide?
This is the crux of the matter. The thing is that adding anything async after pagehide (especially something like an update the rendering step) can break expectations of existing sites in subtle ways that it's hard to envision exactly.
Another difference I'm hearing is that this event won't fire for unloads that aren't part of a cross-document navigation. E.g., closing a window. That's a fairly big difference, although I'm not sure how important it is.
Correct, it would also not fire for cross-origin navigations. This is specifically for a same-origin page transition.
Some early thoughts on ergonomics:
- The navigation API generally tries to treat same- and cross-document navigations on roughly equal footing, with a bias toward same-document navigations if anything. So this event would feel a bit out of place there. But maybe that could be ameliorated with a clear name, e.g.
oncrossdocumentcommit.
SGTM. But maybe navigation.ondocumentchange or navigation.onbeforetransition?
- I like the symmetry of
pageconceal/pagereveal. It might be a bit confusing that it doesn't fire on cases like closing a window, but maybe it's OK.
Yes, the mental model is that concealing/revealing is about gradually hiding/showing. When it's a cross-origin navigation or a window close, the hiding can't be gradual - it's immediate and hence the event doesn't fire.
- It's not clear exactly what the proposed behavior is for when we're about to unload and switch to a cross-origin document. Event fires, with some censored properties? Event doesn't fire?
Event doesn't fire. Once we know this is a cross-origin navigation we immediately activate the new document and unload in the background.
- It might be nice if the properties of this event were a subset of
NavigateEvent, e.g.navigationType,destination,hasUAVisualTransition, maybeformData.
I'm fine with that, with an additional viewTransition.
Thanks!
It does seem possible that you could just add something async after pagehide without breaking much, but I'll leave it up to implementers as to whether they want to take that risk. (It sounds like, at least for Chromium, the answer is no.)
Given that you don't want to fire for a cross-origin document, I think pageconceal on Document is probably the best name. It'd be too weird to have such a specific event that only fires for a small subset of navigations as part of the navigation API, I think.