WICG / view-transitions

https://drafts.csswg.org/css-view-transitions-1/
Other
807 stars 51 forks source link

Clarify what suppressing rendering means in terms of lifecycle updates #171

Open sebmarkbage opened 2 years ago

sebmarkbage commented 2 years ago

In Chrome there's a heuristic to delay paint for 100ms on initial page load if a font is preloaded and optional. This is great because it avoids flashes of invisible text.

I'm not sure how/if this part will ever be spec:ed. Ideally this could also be used with other display modes to avoid the CLS if possible. This is also related to the font-display: critical proposal.

One problem with these heuristics is that they only work for initial load. For SPA transitions it doesn't work. If you navigate to a new page that uses a font there's no way to delay the paint because everything paints as soon as possible - when there's some pending DOM mutations.

We can do some stuff to delay the DOM mutations in frameworks but because detecting if you actually use something requires apply styles which require DOM mutations makes this tricky to implement efficiently and easy to use, because there's no way to create an optimistic DOM tree for what might be rendered unless you have a way to revert any changes.

However, shared-element-transitions do have some of those capabilities at the painting layer. You could effectively delay the animation from starting if painting the new element would be missing a font.

So my proposal is to allow shared element transitions to automatically delay playing the animation for up to 100ms if a new element entering or element with updated text styles/content is still waiting on a font-face to load - under the same heuristics that apply for initial paint. In other words, wait to paint the "target screenshot" until the fonts have loaded or some timeout.

Delaying animations might be somewhat controversial since it can delay the feeling of responsiveness but it can also be better sometimes. Therefore this could be some extra option to the transition that opts into waiting for some heuristic.

jakearchibald commented 2 years ago

Seems like this is already possible, since SET already allows developers to 'hold' the current paint at their discretion:

const transition = new SameDocumentTransition();
transition.prepare(async () => {
  // The previous paint is now held.
  updateTheDOM();
  await document.fonts.ready;
  // The animation starts after this callback has fulfilled.
});
jakearchibald commented 2 years ago

Here's a demo:

No waiting for fonts: https://simple-set-demos.glitch.me/waiting-for-fonts/without-waiting/ With waiting for fonts: https://simple-set-demos.glitch.me/waiting-for-fonts/with-waiting/

The difference between the two is await document.fonts.ready.

jakearchibald commented 2 years ago

For others reading, this should be paired with a timeout of sorts, such as 100ms as @sebmarkbage suggested:

const wait = ms => new Promise(r => setTimeout(r, ms));

const transition = document.createDocumentTransition();
transition.start(async () => {
  updateTheDOM();
  await Promise.race([document.fonts.ready, wait(100)]);
});
sebmarkbage commented 2 years ago

Neat!

Unfortunately, this doesn't seem to work with font-display: optional in my testing (e.g. if you switch to ...&display=optional in the Google Font urls). Because while the transition is waiting it seems like the font gets "used" which triggers its permanent fallback state.

I suspect that's an under specified part of the spec because it's very subtle whether a font has been used or not.

E.g. without transitions you can use this technique (in Chrome at least) to ensure that the font usage gets detected but then is not "shown" and so it doesn't trigger the permanent fallback state:

  document.body.innerHTML = ...;
  const p = document.fonts.ready;
  document.body.style.display = "none";
  await p;
  document.body.style.display = "";

I suspect something similar is necessary for transitions but I shouldn't have to hide it in this case since it's not really painted.

jakearchibald commented 2 years ago

Yeah, in the current implementation, while the paint is 'held', the only thing we're skipping is updating the pixels on the screen. We're going to change that so the render steps don't run at all (as if the display was 0hz).

For example, right now, things like animated gifs will be 'running' in the background. With the change, it will ensure that the gif starts along with the first frame of the transition.

vmpstr commented 2 years ago

Just as a note, we need to verify that the gifs that will be running are going to be in the background. Chromium's implementation drives gif animations from the compositor, so it's not immediately clear to me that skipping paint or skipping rendering altogether would have an effect on those.

jakearchibald commented 2 years ago

@vmpstr does that mean compositor-driven CSS animations won't pause either? That seems a little inconsistent

vmpstr commented 2 years ago

Yes, I believe that's the case

khushalsagar commented 2 years ago

There is an open issue in the spec for clarifying what suppressing rendering means. The last meeting where we discussed this the conclusion was that all animations should be paused otherwise you have this inconsistency of which animation runs depending on the UA's implementation of what's threaded (like GIFs in Chrome's case).

@jakearchibald @vmpstr does that conclusion align with you both?

jakearchibald commented 2 years ago

+1

mmocny commented 1 year ago

Late to this conversation, but, in what ways is this related to image decoding=sync?

As far as I know decoding=sync will also "block rendering", and also somewhere down stream from Paint (at least in chromium), but does not block declarative compositor-driven animations/scrolling updates. I think that means that "Activate" stage is blocked? And it may also have a timeout?

vmpstr commented 1 year ago

(All in Chromium terms) You're right that decoding=sync will block activation until the rasterization of affected images is done. This does mean that ongoing active tree animations will continue producing frames.

For this feature, we're blocking both main and impl frames which means that even the active tree will not produce new frames, after the initial one that requests the snapshot which needs to be forwarded to the gpu stack. This means that active animations, gifs, etc will all be "paused"

vmpstr commented 1 year ago

And it may also have a timeout?

The sync decoding case would not have a timeout. FWIW, the default decoding value is 'auto' and in Chromium that should be equivalent to 'sync' (iirc), so when we're talking about decoding=sync, that's just the default state

mmocny commented 1 year ago

Thanks @vmpstr!


Regarding a question @sebmarkbage asked up top, about how this relates to the font-display: critical -- I was also curious. Here's my understanding (and I'd love corrections if its off).

There are a few distinct uses for the phrase "render-blocking":

  1. Render-blocking resources (which seems already specced)
  2. Anything explicitly preventing updates to the presentation of animation frames via some other form of throttling (as in image decode or SET)
  3. Very broadly, just delays to updates for typical animation frames, i.e. just doing lots of work (long-tasks on main, expensive raster work off-main, etc)

My read of the font-display: critical proposal is it was just asking explicitly about (1).

My read is that SET needs some way to spec (2).

And (3) I know I use use to explain overly long paint-timings (i.e. FCP, LCP, INP, etc) which are specifically due to rendering issues.


There is already the concept of rendering opportunity which is typically how we explain frame throttling -- but HTML update-the-rendering is generally underspecified in terms of parallel rendering. I have previously found this doc to be useful to explain some of it.

We have existing issues in terms of paint timing and element timing spec due to underspecified nature of parallel animation frame updates, which we would like to also be able to address.

khushalsagar commented 1 year ago

SET is going to use render blocking to explain how rendering is suppressed. For cross-document navigations, suppressing rendering of the new Document until is ready for transition will be controlled by "render-blocking".

For the same-document JS API, I was planning to expand the text here to include transition suppressing rendering as one of the reasons a document doesn't have rendering opportunities.

The spec language will need to ensure a transition suppresses rendering for all nested documents as well.

Also this was recently discussed in the issue here which has a summary of the discussion and the resolution.

khushalsagar commented 1 year ago

Closing this issue given the resolution on the csswg issue. Please feel free to continue the discussion there if there are more questions.