Skip to content
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

Proposal: fire an event before the first rendering opportunity after activation #9315

Closed
noamr opened this issue May 21, 2023 · 71 comments · Fixed by #9818
Closed

Proposal: fire an event before the first rendering opportunity after activation #9315

noamr opened this issue May 21, 2023 · 71 comments · Fixed by #9818

Comments

@noamr
Copy link
Contributor

noamr commented May 21, 2023

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 first requestAnimationFrame 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.

@smaug----
Copy link
Collaborator

smaug---- commented May 21, 2023

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.

@noamr
Copy link
Contributor Author

noamr commented May 21, 2023

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:

  1. we didn't want this event to fire during update the rendering.
  2. Requiring to have this rAF in two places is error prone. In the case of view transitions, developers might think that their transitions work, but they would be buggy when the page is restored from BFCache if they have the rAF only in the head. Having a 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

@nt1m nt1m added topic: popover The popover attribute and friends and removed topic: popover The popover attribute and friends labels May 23, 2023
@khushalsagar
Copy link
Contributor

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?

@smaug----
Copy link
Collaborator

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.

@noamr
Copy link
Contributor Author

noamr commented May 26, 2023

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?

@khushalsagar
Copy link
Contributor

@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.

@noamr
Copy link
Contributor Author

noamr commented May 30, 2023

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)

@smaug----
Copy link
Collaborator

smaug---- commented May 31, 2023

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.

@noamr
Copy link
Contributor Author

noamr commented May 31, 2023

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.

@smaug----
Copy link
Collaborator

You said
"The event should be somewhat equivalent to addEventListener('pageshow', () => requestAnimationFrame(...))"

@noamr
Copy link
Contributor Author

noamr commented May 31, 2023

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.

@khushalsagar
Copy link
Contributor

Let's take this up in a separate issue, filed w3c/csswg-drafts#8888. The desired behaviour is needed irrespective to the event proposed here.

@khushalsagar
Copy link
Contributor

This was discussed at CSSWG recently, the notes are on w3c/csswg-drafts#8805 (comment). 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.

@past past added the agenda+ To be discussed at a triage meeting label Aug 8, 2023
@khushalsagar
Copy link
Contributor

@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).

@past past removed the agenda+ To be discussed at a triage meeting label Aug 10, 2023
@vmpstr
Copy link
Member

vmpstr commented Aug 10, 2023

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:

  • either there is no view transition (which is the case here because the timing of the check is correct)
  • or there is no activeViewTransition yet, because the check is not at the correct time, which may be a potential source of developer bugs

/cc @smaug---- @mfreed7

@smaug----
Copy link
Collaborator

smaug---- commented Aug 11, 2023

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)

@noamr
Copy link
Contributor Author

noamr commented Aug 11, 2023

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)

@vmpstr
Copy link
Member

vmpstr commented Aug 14, 2023

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

@noamr
Copy link
Contributor Author

noamr commented Aug 14, 2023

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:

  1. Right before the first rAF
  2. Right after pageshow when reactivating, whether there's a pending animation or not

@vmpstr
Copy link
Member

vmpstr commented Sep 27, 2023

Would readytopresent be better? If we're diving deep into language here, prerender means we render before... something. Kind of like preheat the oven means heat it before baking. My general dislike of the prefix pre aside 😃 I do think that prerender is render before activating and before presenting the frame. I don't know if presentation is an overloaded term in the spec, but that's what it feel like to me. "ready to put a frame up on screen", which also separates itself further from update the rendering which isn't required to put a frame up on screen.

(I'm also happy with other proposals that people have put up though)

@noamr
Copy link
Contributor Author

noamr commented Sep 27, 2023

Would readytopresent be better? If we're diving deep into language here, prerender means we render before... something. Kind of like preheat the oven means heat it before baking. My general dislike of the prefix pre aside 😃 I do think that prerender is render before activating and before presenting the frame. I don't know if presentation is an overloaded term in the spec, but that's what it feel like to me. "ready to put a frame up on screen", which also separates itself further from update the rendering which isn't required to put a frame up on screen.

(I'm also happy with other proposals that people have put up though)

readytopresent brings us back to the fact that we might have already presented something (the last cached frame).
It's hard to get the name to accurately represent the subtleties. That's why a distinct name like pagereveal which roughly means "update/show something that's not necessarily complete" and familiar with pageshow and pagehide appeals to me.

@vmpstr
Copy link
Member

vmpstr commented Sep 27, 2023

Ok, yeah I didn't realize that we're already showing a cached frame. I don't know if pagereveal is much better then? We've already shown a thing that we think is the page. But I guess it means "the real page is revealed"?

@noamr
Copy link
Contributor Author

noamr commented Sep 27, 2023

Ok, yeah I didn't realize that we're already showing a cached frame. I don't know if pagereveal is much better then? We've already shown a thing that we think is the page. But I guess it means "the real page is revealed"?

Yea, the updated content is being revealed.

@zcorpan
Copy link
Member

zcorpan commented Sep 27, 2023

prerender is a separate thing which would be confusing.

So in the bfcache case where there is no new render update step, the event is fired anyway (but no animation frame callback etc), correct? Same timing as pageshow?

@noamr
Copy link
Contributor Author

noamr commented Sep 27, 2023

prerender is a separate thing which would be confusing.

So in the bfcache case where there is no new render update step, the event is fired anyway (but no animation frame callback etc), correct? Same timing as pageshow?

Based on previous comments in this discussion, It would fire at the next render after activation, whenever that happens.
When there is a view-transition, there is a guaranteed first render.

@zcorpan
Copy link
Member

zcorpan commented Sep 27, 2023

Then I think firstframe is clearer.

@khushalsagar
Copy link
Contributor

FWIW I don't think its very expensive to force a "update the rendering" loop after activation to ensure this event is fired after BFCache restore, if that's what authors would expect. Kinda like how registering a raf handler forces a frame. We'll hit an early out fairly quickly, it won't be an expensive operation.

So we should do what authors would expect. @vmpstr made a good point that someone might use this event to register a raf handler, and be surprised that the event never ran after BFCache restore unless something else was damaged.

@noamr
Copy link
Contributor Author

noamr commented Sep 27, 2023

@khushalsagar Yes, it might be better to always fire it after pageshow and exit early from update the rendering if there's nothing else to do. This would fit better with what this event means in terms of user experience.

(In spec terms, this is whether we put this event before or after the early exit for "nothing to do").

If we do this, do you still think firstframe represents it better @zcorpan? I feel that people might not realize that it's the first frame + the first frame after activation

@jakearchibald
Copy link
Collaborator

Yes, it might be better to always fire it after pageshow

(except in the first-load case where pageshow fires far too late)

@noamr
Copy link
Contributor Author

noamr commented Sep 27, 2023

Yes, it might be better to always fire it after pageshow

(except in the first-load case where pageshow fires far too late)

Yes, I meant the reactivation pageshow.

@khushalsagar
Copy link
Contributor

I lean towards "firstrender" over "firstframe", frame seems ambiguous from the spec perspective. "firstrender" is a shorter version of "firstrenderingopportunity" in my mind.

I like the idea of using "first" in the name, conceptually this is the first frame after some specific events in the Document's lifecycle: first load, pre-render activation, BFCache activation. I realize its not obvious what those events are from the term "first" but I can't think of anything more terse. We could go with "firstrenderonactivation". It sounds like activation is a well understood term in the navigation lifecycle to mean that the Document is being presented to the user (as opposed to being in the background).

@noamr
Copy link
Contributor Author

noamr commented Sep 27, 2023

I like firstrender! Also firstanimationframe which is not ambiguous, and perhaps hints that:

  • this doesn't request an animation frame.
  • This is a feature mainly intended for animations

Note that though activation is well understood in the spec, it's not web exposed in APIs so much.

@domenic
Copy link
Member

domenic commented Sep 28, 2023

Wait... I thought I tried to make it clear... render should be off the table. Because during prerendering, the first render happens before the user sees anything.

Similarly for animationframe. requestAnimationFrame() can be called during prerendering. So there's no sense in which this event would fire for the first animation frame.

@noamr
Copy link
Contributor Author

noamr commented Sep 28, 2023

Wait... I thought I tried to make it clear... render should be off the table. Because during prerendering, the first render happens before the user sees anything.

Similarly for animationframe. requestAnimationFrame() can be called during prerendering. So there's no sense in which this event would fire for the first animation frame.

How about we change the behavior slightly, to also depend on visibility, and call it firstvisibleframe?
So the definition would be:
after activation, fire on the next render opportunity where the page is visible and has updates.

This would perhaps clarify the purpose of this event - it's the first time where you can update the rendering and have it be presented instantly.

If there are reasons against this behavior change, I would suggest to move back to pagereveal rather than try to compose existing concepts. We can even have a document.revealed boolean that means the same thing (document is active and had its first fully-active rendering opportunity).

@domenic
Copy link
Member

domenic commented Sep 28, 2023

Either firstvisibleframe or pagereveal sounds good to me. As long as there's no render in the name.

@noamr
Copy link
Contributor Author

noamr commented Sep 28, 2023

Either firstvisibleframe or pagereveal sounds good to me. As long as there's no render in the name.

@zcorpan @khushalsagar ?

@jakearchibald
Copy link
Collaborator

jakearchibald commented Sep 28, 2023

Given the behaviour is:

once a document becomes active, the event is fired right at the beginning of the next render steps

  • firstvisibleframe doesn't make it clear that 'first' resets after a document becomes inactive. In fact, in terms of naming, it's similar to visibilitychange, where visibility means something different to active.
  • pagereveal doesn't make it clear that it runs within the render steps, but its proximity to pagehide and pageshow mean it's somewhat related to activation. That said, it isn't particularly common for events to make it clear if they run in the render steps or not. For example, pointermove, resize, scroll happen in the render steps too.

Based on that, pagereveal sounds ok.

@zcorpan
Copy link
Member

zcorpan commented Sep 28, 2023

I'm fine with pagereveal

aarongable pushed a commit to chromium/chromium that referenced this issue Sep 29, 2023
See arguments in
whatwg/html#9315

Bug: 1466250
Change-Id: I4e722d3d71a707352054f9408ab01fb54dbf2954
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4902322
Commit-Queue: David Bokan <bokan@chromium.org>
Reviewed-by: Khushal Sagar <khushalsagar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1203002}
@noamr noamr mentioned this issue Oct 16, 2023
5 tasks
domenic pushed a commit that referenced this issue Dec 15, 2023
The pagereveal event is fired at the beginning of the first rendering opportunity after activation (initial load or reactivation). It is a way for the author to execute some JS that affects the presentation "just in time" for the first frame.

If there is an inbound cross-document view transition, the reveal event holds a reference to the ViewTransition object.

Closes #9315.
rubberyuzu added a commit to rubberyuzu/html that referenced this issue Dec 21, 2023
Add pagereveal event

The pagereveal event is fired at the beginning of the first rendering opportunity after activation (initial load or reactivation). It is a way for the author to execute some JS that affects the presentation "just in time" for the first frame.

If there is an inbound cross-document view transition, the reveal event holds a reference to the ViewTransition object.

Closes whatwg#9315.

Use UA styles rather than prose to define <input> clip

The previous prose to make `overflow` act as `visible` with regards to other CSS features but still clip didn't work well with e.g. `text-overflow: ellipsis`. CSS now has a standard way to do what `input` buttons need, i.e. clip and also not affect interaction with `vertical-align`.

Fixes whatwg#9976.

Forbid nesting <details> in the same exclusive accordion

Fixes whatwg#9968.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

12 participants