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

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

Open
sebmarkbage opened this issue Jul 27, 2022 · 16 comments
Open
Labels
open-spec-issue Unaddressed issue in the spec

Comments

@sebmarkbage
Copy link

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Author

sebmarkbage commented Jul 28, 2022

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
Copy link
Collaborator

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
Copy link
Collaborator

vmpstr commented Jul 29, 2022

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
Copy link
Collaborator

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

@vmpstr
Copy link
Collaborator

vmpstr commented Jul 29, 2022

Yes, I believe that's the case

@khushalsagar
Copy link
Collaborator

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?

@khushalsagar khushalsagar added the open-spec-issue Unaddressed issue in the spec label Aug 16, 2022
@khushalsagar khushalsagar changed the title Delay animation on font loading for the new content Clarify what suppressing rendering means in terms of lifecycle updates Aug 16, 2022
@jakearchibald
Copy link
Collaborator

+1

@mmocny
Copy link

mmocny commented Oct 7, 2022

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
Copy link
Collaborator

vmpstr commented Oct 7, 2022

(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
Copy link
Collaborator

vmpstr commented Oct 7, 2022

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
Copy link

mmocny commented Oct 7, 2022

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
Copy link
Collaborator

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
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
open-spec-issue Unaddressed issue in the spec
Projects
None yet
Development

No branches or pull requests

5 participants