Closed noamr closed 11 months ago
This problematic. Nothing may be re-rendered when coming out of the bfcache, as an example, if the rendering is still cached. Why is rAF not enough? That should force rendering if something in DOM is changed in a way which affects the page visually.
This problematic. Nothing may be re-rendered when coming out of the bfcache, as an example, if the rendering is still cached. Why is rAF not enough? That should force rendering if something in DOM is changed in a way which affects the page visually.
hmm interesting point about cached rendering for BFCache.
It's possible to simulate this by calling rAF both in the head and in pageshow
. Two downsides:
update the rendering
.reveal
event and pointing to it as the place to put transition-related DOM changes seems more solid.A possible solution is to fire this event if rendering is still cached, but we force it if there's a cross-document transition.
An alternative is to call it something else, and always fire it right after pageshow
and before the document's first regular rAF
callback, regardless of whether there's a rendering opportunity in practice.
/cc @khushalsagar
A possible solution is to fire this event if rendering is still cached, but we force it if there's a cross-document transition.
Did you mean "not fire" this event if rendering is still cached?
Nothing may be re-rendered when coming out of the bfcache, as an example, if the rendering is still cached.
Funny enough, we ran into this while prototyping. The bug is still being fixed. You can see it in action on chrome canary (<116.0.5791.0) if you enable VT on navigations, load https://foil-persistent-bookcase.glitch.me/page2.html, click navigate then go back/fwd. Because the browser displays the cached rendering of the restored page before its first rAF, you get a flash of the restored page and then an animation. The fix is simply to not display the cached rendering.
The reason being that the restored page will start rendering with a tree of view-transition pseudo-elements that show a screenshot of the old page. Authors can then customize the animation when the restored page starts rendering, same as what would happen with an SPA back/fwd nav.
There is no standardization around displaying the restored page's cached rendering (to my knowledge). Looks like with this feature we'll have to explicitly say, "the browser shouldn't display any cached rendered output of the restored page, first frame must be the output of the first rendering opportunity after pageShow
". @smaug---- Does that sound reasonable?
It doesn't really. Isn't tab switching quite similar case. Browsers do throttle the background tabs heavily, but when making such tab foreground, something needs to be painted, asap, even if there is slow running JS running in that background tab. Bfcache isn't too different to that. We don't want to let the page to postpone showing the page to the user.
It doesn't really. Isn't tab switching quite similar case. Browsers do throttle the background tabs heavily, but when making such tab foreground, something needs to be painted, asap, even if there is slow running JS running in that background tab. Bfcache isn't too different to that. We don't want to let the page to postpone showing the page to the user.
So do you display the tab before the pageshow
event? Because that will cause a flicker if you do and pageshow
changes the DOM, right?
@smaug----, I agree that we don't want this policy in general. Chrome has similar behaviour, if you switch tabs (or go to a page restored from BFCache) we flip to the cached rendering of the new page if available. If the transition to the new page is going to be a direct flip, it makes sense to do it asap.
I was hoping to carve out an exception if there is a ViewTransition. In this case it makes sense to keep the last rendered output of the old page onscreen until the restored page draws a frame. Because the restored page is going to start with a snapshot of the old page's contents (wrapped up in view-transition pseudo-elements) and customize the transition instead of a direct flip. So the spec should mandate this behaviour only if there is a ViewTransition.
@noamr you're right that if the page chooses to update the DOM in pageshow
, you'd see a "flicker". But we can't really avoid that. Multiple browsers have UI which need to show screenshots of inactive pages (tab switcher on Chrome on Android or back/forward swipe on Safari come top of mind). Browser UX can paper over such flickers by animating between a screenshot and live DOM.
We don't want to let the page to postpone showing the page to the user.
... In situations where navigation is explicitly different from tab-switching, i.e. there's a pageshow
or reveal
event, this is potentially exactly what the developer wants.
However, delaying is not the problem here. In the case of having cached rendering on BFCache traversal and no view-transition, we can fire that event without re-rendering. This would keep this "cached rendering" thing an implementation detail that's not observable.
The event should be somewhat equivalent to addEventListener('pageshow', () => requestAnimationFrame(...))
with subtleties that make it distinct enough (also fired on initial render-unblock, fired before update-the-rendering)
So if pageshow listener does slow things, like it often may do, rendering might be postponed significantly and user experience would be worse, since user wouldn't see anything. Or I'm not sure what would be visible... the previous page, while the current is already the one which got out of bfcache...that might be a security issue then. We also don't want flickers when a page comes out of the bfcache, so something reasonable should be painted.
So if pageshow listener does slow things, like if often may do, rendering might be postponed significantly and user experience would be worse, since user wouldn't see anything. Or I'm not sure what would be visible... the previous page, while the current is already the one which got out of bfcache...that might be a security issue then. We also don't want flickers when a page comes out of the bfcache, so something reasonable should be painted.
Not sure how pageshow
is related. Not suggesting to change the behavior of pageshow
.
In the case of BFCache-with-cached-snapshot, reveal
would be called right before pageshow
and wouldn't change anything about the lifecycle. If the UA decided to show a snapshot before then, great! No difference.
In the case of same-origin view transitions, where both documents opted in to view transitions, we'll keep showing the old document until the transition is ready to start.
In initial loading, the event will be fired at the moment rendering is unblocked.
You said "The event should be somewhat equivalent to addEventListener('pageshow', () => requestAnimationFrame(...))"
You said "The event should be somewhat equivalent to addEventListener('pageshow', () => requestAnimationFrame(...))"
Right, it would be somewhat equivalent to that in behavior. But not suggesting to change the behavior of pageshow
. The event should only affect the lifecycle if there are (same-origin) view-transitions, otherwise it's the same as "pageshow" or something like waiting for all render-blocking styles/scripts to load.
Let's take this up in a separate issue, filed https://github.com/w3c/csswg-drafts/issues/8888. The desired behaviour is needed irrespective to the event proposed here.
This was discussed at CSSWG recently, the notes are on https://github.com/w3c/csswg-drafts/issues/8805#issuecomment-1640688426. There is general support of the idea but we need a discussion at HTML WG to narrow down the specifics of when the event is dispatched and whether it should be VT specific.
@noamr's feedback below.
Re: whether this event should fire only when there is a transition, one of the use-cases to fire it every time is detecthing whether is a transition to execute code which is deferred until the end of the transition. For example:
function hideLoader() { ... }
document.addEventListener("reveal", event => {
if (event.viewTransition) {
event.viewTransition.finished.then(hideLoader);
} else {
hideLoader();
}
});
A transient loading UI which is hydrated with content when the transition is finished. If the event is not fired when there is no transition then authors will have to write awkward code to track whether the event was fired and run hideLoader()
immediately on rAF if it was not fired (indicating no transition).
At the HTML spec triage, we've talked about a possibility of the rAF based solution. Below roughly would be the equivalent required for the same effect (I actually don't know of a sure way to hook into the first rAF, but I think just doing requestAnimationFrame works)
<script>
function runRevealEvent() {
if (document.activeViewTransition) {
...
}
...
}
// For the "very first rAF" (this works, right?)
requestAnimationFrame(runRevealEvent);
// For BFCache activation
addEventListener("pageshow", e => { if (e.persisted) { runRevealEvent() } });
</script>
With the reveal event, the code would be the following:
<script>
// This fires at "the right time" whether or not it's a new navigation
// or BFCache activation
addEventListener("reveal", e => {
if (e.viewTransition) {
...
}
...
});
</script>
To make my case: the script here is by no means complicated, but for the user adopting view transitions, in the latter case, the only way to get the viewTransition object is to listen to this event, and this event will work correctly for BFCache and new navigation cases. This seems hard to get wrong.
In the former, it seems easy enough to miss the BFCache case. Also, the if (document.activeViewTransition)
check has two meanings:
/cc @smaug---- @mfreed7
One very nice feature of rAF based solution is that it is basically an explicit request to paint. Adding an event listener is not. And when coming out of bfcache one might not paint normally, because it would be just useless if the rendering has been cached. Sure, implementation might optimize behavior so that painting would happen if event listener is there and avoid useless paints without it.
And in the first example the question about 'very first rAF...?' applies to adding the event listener too. If rendering is blocked in some way (through the explicit renderBlocking or via the old style heuristics, which are somewhat spec'ed), I'd expect both to work.
Tab switching is still something a bit unclear to me. If a page is loaded in a background tab, it might not be painted at all before the tab is brought to foreground. I guess I don't know how view transitions are supposed to work in that case.
(And yesterday when I talked about FF not implementing render blocking, I meant explicit blocking="render" . FF bug)
One very nice feature of rAF based solution is that it is basically an explicit request to paint. Adding an event listener is not. And when coming out of bfcache one might not paint normally, because it would be just useless if the rendering has been cached. Sure, implementation might optimize behavior so that painting would happen if event listener is there and avoid useless paints without it.
Why does this need to be an explicit request to paint? You might want the event without wanting to paint.
The problem with the rAF-based solution is that it's very easy to overlook BFCache-restore, creating bugs. you'd always have to do the following, otherwise your animation would sometimes work and sometimes not:
/* in <head> */
function animateTransitionWithWebAnimations() { if (document.inboundViewTransition) { ... } }
requestAnimationFrame(animateTransitionWithWebAnimations);
document.addEventListener("pageshow", animateTransitionWithWebAnimations);
It's OK to do this and educate people about it but it seems awkward.
And in the first example the question about 'very first rAF...?' applies to adding the event listener too. If rendering is blocked in some way (through the explicit renderBlocking or via the old style heuristics, which are somewhat spec'ed), I'd expect both to work.
Tab switching is still something a bit unclear to me. If a page is loaded in a background tab, it might not be painted at all before the tab is brought to foreground. I guess I don't know how view transitions are supposed to work in that case.
There are no view transitions in tab switching. I think the reveal event should apply if it's the first render opportunity in that tab.
(And yesterday when I talked about FF not implementing render blocking, I meant explicit blocking="render" . FF bug)
The need to have two different ways to hook into rAF (initial frame + pageshow for persisted case) is going to make this approach error prone. I still prefer that we consider the reveal event (perhaps named something less similar to pageshow).
I also want to clarify that the rAF based solution isn't "free". That is, it isn't only using existing features: it also requires activeViewTransition (or inboundViewTransition as @noamr called it). So I think the cost between the two solutions is similar.
The pro of the rAF based solution is that we don't need to worry about event timing, but the con is that the developer can get the pattern wrong pretty easily (omitting BFCache case for example).
The pro of the "reveal" event is that it's hard for the developer to get it wrong. The con is that it may be trickier to specify in all cases (e.g. tab switching).
Let me know if you disagree with this assessment. If it's correct, then I think we should prioritize the ease of use for the developer
The pro of the "reveal" event is that it's hard for the developer to get it wrong. The con is that it may be trickier to specify in all cases (e.g. tab switching).
The challenge with the reveal event is that when activating from BFCache there might not be an animation frame at all (on Firefox). This is an actual issue. Perhaps we should fire the reveal event:
pageshow
when reactivating, whether there's a pending animation or notCorrect me if I'm wrong, but the reveal event does not need an animation frame, it just needs to fire before the first animation frame would happen (like option 2 in your example). We haven't discussed the timing of the event in great detail yet. With view transitions, the animation frame will happen but for reasons unrelated to the reveal event
Correct me if I'm wrong, but the reveal event does not need an animation frame, it just needs to fire before the first animation frame would happen (like option 2 in your example). We haven't discussed the timing of the event in great detail yet. With view transitions, the animation frame will happen but for reasons unrelated to the reveal event
In the current spec draft it's at the first rAF after page init/activation. But yes, this can be discussed and resolved. I don't think it's THAT complex, and if we don't figure it out developers would have to...
@smaug---- @marcoscaceres
Just to make sure we’re on the same page with the use-case. A key motivation behind this proposal is to let authors customize the View Transition based on the state of the DOM at first paint, which itself relies on which sub-resources are fetched at that point.
For example, you’re going from page A to B where one image morphs into another. In page A you added a prefetch link to B’s image. When page B paints, the image on that side may/may not be available. So the author could design the transition as:
// Track whether the target image has been loaded.
let imageLoaded = false;
myImage.onload = () => { imageLoaded = true; };
function setUpTransition() {
if (imageLoaded) {
// If the image has loaded, tag it with a view-transition-name to create a morph animation.
myImage.style.viewTransitionName = target;
} else {
// As a fallback, just fade-in the whole page.
setUpRootOnlyTransition();
}
}
The browser walks the DOM to discover elements with a view-transition-name
on B once (for new page load or activation). The exact logic is spec’d in the capture new state algorithm. This means that setUpTransition() must run before this algorithm.
The way this works for same-document transitions is as follows:
Author calls document.startViewTransition(updateCallback)
where updateCallback can be async.
Browser captures the old DOM state and dispatches updateCallback where the author updates the DOM to the new state.
Once updateCallback resolves, the browser immediately runs “capture new state”, spec’d here.
So authors can write code like this. The callback gives them an entry point to know state on first paint and set up transitions accordingly.
document.startViewTransition(async () => {
await fetchNewResourcesOrTimeout();
updateDOM();
setUpTransition();
});
For cross-document transitions, fetchNewResourcesOrTimeout()
and updateDOM()
conceptually map to the browser parsing the new Document and waiting on render-blocked resources. But once that’s done, authors need an entry point for setUpTransition(). So the choices are:
Add a new event which gives them that entry point, the proposal on this issue. Then “capture new state” can run immediately after the event is dispatched.
Assume authors will use rAF() for setUpTransition() and move “capture new state” to after dispatching the rAF callbacks here.
I prefer 1 for consistency with the timing used for same-document transitions and the awkwardness with tracking the right rAF. I can be convinced for 2 but I’m not seeing how that’s better.
An alternate way to do this would be for authors to add load event listeners for each resource they care about and update the DOM whenever an event fires based on the current state. For example,
setUpRootOnlyTransitionForNow();
myImage.onload = () => {
myImage.style.viewTransitionName = target;
setUpImageTransition();
};
But that feels way less ergonomic, and will be required only with cross-document transitions because the requisite hook is not available.
One very nice feature of rAF based solution is that it is basically an explicit request to paint. Adding an event listener is not. And when coming out of bfcache one might not paint normally, because it would be just useless if the rendering has been cached.
I might be misunderstanding what you meant but I think it’s about the API contract for pages restored from BFCache:
Does reveal
imply the browser must repaint the page even if it has a cached frame?
No, reveal
is the same as pageShow
in that regard. If the timing of pageShow
was actually before the first rendering opportunity, we wouldn’t need to introduce a new event. :)
Does reveal
imply the browser must paint and show the DOM state after it's dispatched, instead of showing a cached frame?
No. We’ve made an exception for when there is a View Transition (see https://github.com/w3c/csswg-drafts/issues/8888). In that case, the browser will naturally have to repaint the restored Document.
But otherwise there is no way on the platform (to my knowledge) for a restored page to prevent the browser from displaying a cached frame. I’m not aware of any use-cases for such a capability, but if there are any subscribing to this event shouldn’t be how that’s expressed.
and if we don't figure it out developers would have to...
This is a good way of putting it, and it's one of my main arguments for having a new event
The problem with "reveal" event is that if such listener is added, UA is forced to delay showing the restored-from-bfcache page to the user. UA would effectively need to ask permission from the web page when it can be shown. And handling the event might take quite a bit time. rAF does have similar issues, but so far nothing has guaranteed that rAF gets called when the page comes out of bfcache (which of course would be problematic for the view transitions use case).
Do we really want the main thread of the web page do anything related to the painting (like modify DOM) when coming out of bfcache? I'd expect view transition stuff being pushed to some other process (parent/compositor/whatever-it-is-called-in-different-browsers) and then we can guarantee smooth and fast transitions.
The problem with "reveal" event is that if such listener is added, UA is forced to delay showing the restored-from-bfcache page to the user. UA would effectively need to ask permission from the web page when it can be shown.
It shouldn't change the behavior in this way, unless there is an actual view transition. It's more similar to pageshow
in the activation case. When there is no view transition, go ahead and present the page, and fire the event - it's there as a FYI so that you know that there is no view transition.
Do we really want the main thread of the web page do anything related to the painting (like modify DOM) when coming out of bfcache? I'd expect view transition stuff being pushed to some other process (parent/compositor/whatever-it-is-called-in-different-browsers) and then we can guarantee smooth and fast transitions.
Yes. The CSS needs to change to enable the pseudo-elements etc. And in the case of the reveal event when there is a view transition, perhaps the transition is going to be animated with the web animation API.
To clarify how this can work in the different scenarios:
reveal
event. @noamr don't think that's what you meant but just confirming. The timing of the event will be the same in BFCache/pre-render cases irrespective of whether there is a transition and it must be before first rAF after activation (whether before or after pageShow
).
The only difference is that in the View Transition case, the first frame displayed for the new Document must be painted on the main thread which by design will include updates in the rAF callback. Even if this event didn't exist, that contract is still there.
@noamr don't think that's what you meant but just confirming. The timing of the event will be the same in BFCache/pre-render cases irrespective of whether there is a transition and it must be before first rAF after activation (whether before or after
pageShow
).The only difference is that in the View Transition case, the first frame displayed for the new Document must be painted on the main thread which by design will include updates in the rAF callback. Even if this event didn't exist, that contract is still there.
Yes exactly. My comment was a bit confusing in hindsight, will revise
If reveal
fires at the same time as pageshow
, except for the stupid case where pageshow
fires after load
, maybe call it pagereveal
, and give the event .persisted
.
Then, developers and just use pagereveal
as the opposite of pagehide
and forget pageshow
ever existed.
If
reveal
fires at the same time aspageshow
, except for the stupid case wherepageshow
fires afterload
, maybe call itpagereveal
, and give the event.persisted
.Then, developers and just use
pagereveal
as the opposite ofpagehide
and forgetpageshow
ever existed.
I'm fine with all of this.
The semantics of (page)reveal is a bit weird if it is guaranteed to fire before first paint only when used with view transitions.
Agreed. I hadn't realised that was the proposal.
We can either:
pageshow
handler or so)I think we're going in circles between these.
My tendency is with (1) because it allows developers who build pages with view-transition to handle the case of a navigation where an expected view transition didn't happen.
OK speaking with @smaug---- on Matrix, perhaps we could do something different: Fire this event only when the current page has opted in to cross-document view transitions, but regardless of whether there is an actual view transition. It would need a different name. It would cover this use case without causing any overhead/confusing for non-VT cases.
One option is that it would always have a ViewTransition
, but that it would be already in finished state if the previous document hasn't opted in, with some indicator of that.
Perhaps to avoid confusion, it could be a matter of naming? e.g. beforerenderingupdate
, which doesn't necessarily imply "before first paint". @smaug---- @jakearchibald
I'm good with beforerenderingupdate
as well. I strongly feel that only dispatching it when the current Document opt-ins to VT is unnecessary complexity. The API contract options are:
Irrespective of the 2 options above, whether the first frame displayed by the browser for this Document is something cached or output of first rendering opportunity is the same. If the problem with reveal
is that the name seems to imply no cached output can be used then we can do a better name. Otherwise 2 is making this hard for implementors and adding no benefit for authors.
I still think option (1) would make things a lot simpler for developers, and we should find the right name for the event to avoid confusion.
e.g. beforefirstrender
, readytorender
, canrender
. I still think that reveal
doesn't necessarily imply "before first paint", definitely it doesn't imply that more than pageshow
. We're showing some old state and revealing the new one.
But in either case, I'm OK with finding a name that everyone is comfortable with and means "before the first rendering opportunity after a navigation"
As an FYI: adding a hook on the navigation API was considered in https://github.com/WICG/navigation-api/issues/256. Chatting with @khushalsagar though that doesn't handle the motivating reason for the reveal
event, which is that it'd require authors to explicitly and separately handle BFCache which is likely to be mishandled - so I think we're leaning towards the event at this point.
So, we discussed it at TPAC and seemed like we received a go. @zcorpan @smaug---- can we treat it as a positive position from Mozilla as part of the view transitions feature? same for @annevk for webkit. I can open a new standards-position thing if it helps but I see this as part of view-transitions.
Regarding names: my current favorite is readytorender
or pagereadytorender
(or pagereveal
). The "page" prefix mentally connects it with pageshow
and pagehide
as a page-lifecycle method of sorts.
Also it was not decided in TPAC whether the event should fire before the first rendering update or at the beginning of that update phase. Currently leaning towards the latter for simplicity, perhaps we can resolve this on the PR?
That's probably okay, but please make sure it's tracked as part of https://bugs.webkit.org/show_bug.cgi?id=259055.
Sorry for realizing this so late, but I think "reveal" or something similar related to visibility is going to be better than "render". The reason is that prerendering can definitely perform rendering while off screen. (It's right in the name, actually!) Chromium currently doesn't, but we'd like to, to close the performance gap between our current prerendering implementation and a truly-instant experience.
So, I think it'd be strange if you got an event like readytorender
far after the rendering-to-pixels process had already started, i.e. after requestAnimationFrame()
s had already started firing and other such things.
I also still like reveal.. Perhaps pagereveal
to mentally link it with pageshow
and pagehide
.
The concerns about reveal
were that it's confusing in the case of displaying a cached image of the page, but I think that "displaying a cached image and then revealing the new state" is legit. Revealing means "showing gradually" which is exactly what this is about.
@smaug----, @zcorpan?
It might be helpful to restate when this event actually fires.
Is it as simple as "once a document becomes active, the event is fired before the end of the next render steps"?
It's a good point that this might not run before the user is presented a visual of the page, which may be a cached rendering or a prerender.
It might be helpful to restate when this event actually fires.
Is it as simple as "once a document becomes active, the event is fired before the end of the next render steps"?
It's "once a document becomes active and has a rendering opportunity, the event is fired right at the beginning of the next render steps"
It's a good point that this might not run before the user is presented a visual of the page, which may be a cached rendering or a prerender.
Correct.
Ok, so it could fire as part of the update rendering. Does that mean in practice that update rendering needs to happen at some point after restoring from bfcache? Since wouldn't it be rather surprising to never get the event (since nothing guarantees there is update rendering step after coming out of bfcache)
@smaug---- hmmmmmm, it might be fine that it fires along with the next frame (which I guess could be never). View transitions would request a render on document activation.
Ok, so it could fire as part of the update rendering. Does that mean in practice that update rendering needs to happen at some point after restoring from bfcache? Since wouldn't it be rather surprising to never get the event (since nothing guarantees there is update rendering step after coming out of bfcache)
Spec-wise, there is an update the rendering phase after every task. Implementation-wise, we could either fire it when we are indeed going to render, or schedule a single rendering update only if this event has a listener.
(FWIW, the spec for update rendering will likely need to change anyhow if we ever want to get scheduling API to the HTML spec. And all the implementations have separate tasks to do rendering update, so the spec doesn't match reality.)
Sure. but in this case I'm OK with putting this event after the part where we exit early if the document doesn't need to be updated. So this would match the behavior of "this event is fired after activation, when there is both a rendering opportunity and the document needs to be rendered"
So I guess the long name is frameafteractivate
(assuming it's clear that doesn't mean every frame after activate)
The problem: we have several lifecycle/visibility events, like
pageshow
, but none of them take progressive rendering into account. There is currently no event that is fired before the first rendering opportunity after activation (either due to a new document or BFCache/prerender reactivation).This is needed for cross-document view transitions, as well as for metrics, and solving this can allow the developer to perform "last minute" DOM changes after the document is initialized but before it's rendered.
The proposal: Expose an event (
reveal
?beforepageshow
?) that is guaranteed to be called before the firstrequestAnimationFrame
callback but after activation and after a document is no longer render-blocked.When a cross-document view transition is present, this event would include a reference to the ViewTransition object, which would allow the new document to observe when the transition is finished, skip it, or potentially extend it. See explainer.